diff --git a/.github/actions/download-sdk-runtime/action.yml b/.github/actions/download-sdk-runtime/action.yml new file mode 100644 index 0000000000..d17f6c5e56 --- /dev/null +++ b/.github/actions/download-sdk-runtime/action.yml @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +name: "Download SDK runtime" +description: "Download an SDK runtime artifact with at most three attempts; fail after exhaustion." +inputs: + artifact-name: + description: "Name of the target-specific runtime artifact." + required: true + path: + description: "Directory in which to download the artifact." + required: true +runs: + using: "composite" + steps: + - name: Download runtime artifact (attempt 1/3) + id: download-runtime + uses: actions/download-artifact@v8 + continue-on-error: true + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.path }} + - name: Download runtime artifact (attempt 2/3) + id: download-runtime-retry-1 + if: steps.download-runtime.outcome == 'failure' + uses: actions/download-artifact@v8 + continue-on-error: true + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.path }} + - name: Download runtime artifact (attempt 3/3) + if: steps.download-runtime.outcome == 'failure' && steps.download-runtime-retry-1.outcome == 'failure' + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.path }} diff --git a/.github/actions/run-alpine-tests/action.yml b/.github/actions/run-alpine-tests/action.yml index 9dbb5e0ac4..67a7cf9767 100644 --- a/.github/actions/run-alpine-tests/action.yml +++ b/.github/actions/run-alpine-tests/action.yml @@ -1,5 +1,5 @@ # Runs an SDK command inside x64 Alpine and verifies the container is -# executing on musl with the requested transport. +# executing on musl. name: "Run Alpine SDK Tests" description: "Run an SDK test command in an x64 Alpine container." inputs: @@ -13,22 +13,18 @@ inputs: description: "SDK root inside the container." required: false default: "/workspace" - transport: - description: "SDK transport to test: default or inprocess." - required: true command: description: "Language-specific setup and test command." required: true runs: using: "composite" steps: - - name: Run ${{ inputs.transport }} tests in Alpine + - name: Run SDK tests in Alpine shell: bash env: ALPINE_TEST_COMMAND: ${{ inputs.command }} ALPINE_TEST_IMAGE: ${{ inputs.image }} ALPINE_TEST_SDK_ROOT: ${{ inputs.sdk-root }} - ALPINE_TEST_TRANSPORT: ${{ inputs.transport }} ALPINE_TEST_WORKDIR: ${{ inputs.workdir }} run: | script_path=$(mktemp "$RUNNER_TEMP/copilot-sdk-alpine-test.XXXXXX") @@ -43,11 +39,6 @@ runs: test "$(uname -m)" = "x86_64" ldd --version 2>&1 | grep -qi musl - case "$COPILOT_SDK_TEST_TRANSPORT" in - inprocess) export COPILOT_SDK_DEFAULT_CONNECTION=inprocess ;; - default) unset COPILOT_SDK_DEFAULT_CONNECTION ;; - *) echo "Unknown transport: $COPILOT_SDK_TEST_TRANSPORT" >&2; exit 1 ;; - esac SCRIPT printf '%s\n' "$ALPINE_TEST_COMMAND" >> "$script_path" chmod +x "$script_path" @@ -68,6 +59,5 @@ runs: --env RUST_BACKTRACE \ --env RUSTDOCFLAGS \ --env RUSTFLAGS \ - --env COPILOT_SDK_TEST_TRANSPORT="$ALPINE_TEST_TRANSPORT" \ "$ALPINE_TEST_IMAGE" \ /bin/sh /tmp/copilot-sdk-alpine-test.sh diff --git a/.github/skills/sdk-java-e2e-test/SKILL.md b/.github/skills/sdk-java-e2e-test/SKILL.md index 7ea8f277d9..1e074c027d 100644 --- a/.github/skills/sdk-java-e2e-test/SKILL.md +++ b/.github/skills/sdk-java-e2e-test/SKILL.md @@ -9,17 +9,12 @@ description: "Use this skill when creating a Java SDK surface-area E2E integrati These instructions work in both the runtime monorepo and the standalone public SDK repository. From the repository root, use `src/sdk` as the SDK root when -that directory exists; otherwise use the repository root itself. All paths and -commands below are relative to that SDK root, and command examples should be -run from it. See the companion `examples.md` for complete snapshot and test -examples. - -This skill covers the complete workflow for adding a new Java failsafe -integration test backed by a handcrafted YAML snapshot for the replay proxy. -Use it only when the new E2E tests Java SDK surface area; shared runtime -functionality available entirely through the SDK should instead be covered -in the TypeScript SDK suite (`nodejs/test/e2e/` under the SDK root, or -`src/sdk/nodejs/test/e2e/` from the runtime repository root). +that directory exists; otherwise use the repository root itself. All paths below +are relative to that SDK root. + +Use this skill only when the new E2E tests Java SDK surface area; shared +runtime functionality available entirely through the SDK belongs in the +TypeScript SDK suite (`nodejs/test/e2e/`). ## Overview @@ -27,43 +22,43 @@ The Java E2E tests use a **replay proxy** (`test/harness/replayingCapiProxy.ts`) that intercepts HTTP calls to the Copilot API and returns pre-recorded responses from YAML snapshot files. This avoids needing real authentication in CI. -**Key constraint:** Java's `CapiProxy.java` always sets `GITHUB_ACTIONS=true` -(line 104), which forces the replay proxy into read-only mode. You **cannot** -record snapshots by running Java tests — you must handcraft the YAML. +**Key constraint:** Java's `CapiProxy.start()` always sets `GITHUB_ACTIONS=true`, +which forces the replay proxy into read-only mode: a Java test cannot record a +snapshot, and every request must match an existing one. ## Step-by-Step Workflow ### Step 1: Choose a snapshot category and snapshot base name - Category = a directory under `test/snapshots/` (e.g., `system_message_sections`) -- Snapshot base name = the exact filename stem to use (already lowercase/underscore-separated), - e.g., `should_use_replaced_identity_section_in_response` +- Snapshot base name = the exact filename stem, already lowercase snake_case, + e.g., `should_use_replaced_identity_section_in_response`. Pass it verbatim to + `configureForTest`: it lowercases and replaces non-alphanumerics with `_` but + does not split camelCase, so `myTestMethod` would look for `mytestmethod.yaml`. - Resulting file: `test/snapshots//.yaml` -### Step 2: Create the YAML snapshot file - -The format is: - -```yaml -models: - - claude-sonnet-5 -conversations: - - messages: - - role: system - content: ${system} - - role: user - content: - - role: assistant - content: -``` - -**Rules:** -- `${system}` is a placeholder that matches ANY system message content -- `${workdir}` in tool arguments is substituted with the actual temp workDir -- Each conversation entry represents one request-response exchange -- For multi-turn, add multiple conversation entries -- For tool calls, include `tool_calls` on assistant messages and `role: tool` for results -- The user content must **exactly match** what your test sends (after normalization) +### Step 2: Use a recorded snapshot + +The SDK guide forbids hand-authored model responses (`CONTRIBUTING.md`, +"Recording and replaying SDK tests"). Reuse a capture that another SDK suite +recorded for the same conversation, as most Java ITs do; if none exists, record +one through the TypeScript SDK suite with the same snapshot name and prompt, +following that guide. + +**Replay rules:** +- Every request's system message is replaced with `${system}` before matching, + so system content is never compared, and an assertion on the reply cannot + prove that a system-message setting reached the model. +- User content must **exactly match** the snapshot's (after normalization), so + copy the prompt from the YAML. +- `${workdir}` stands for the test's temp workDir in tool arguments and results. +- A request matches when its messages equal a conversation up to an assistant + message; the proxy replies with that assistant message and any assistant + messages directly after it. +- A request that matches nothing fails. The proxy's stderr, echoed as + `[CapiProxy stderr]`, reports "No cached response found" with the first + mismatching message per conversation, and `configureForTest` logs the prompts + the snapshot expects. ### Step 3: Create the Java IT test class @@ -146,9 +141,11 @@ pnpm run generate:sdk:java pnpm run build:cli ``` -The facade's runtime paths apply only to its child process. For a focused Maven -run, set the same-checkout wrapper path explicitly, then use the `verify` -lifecycle so test-resource setup and `failsafe:verify` both run: +`pnpm run test:sdk:java` runs the whole suite and takes no test filter. For a +focused Maven run, pass the same-checkout wrapper explicitly (without +`copilot.cli.path` the tests use the release pinned by `nodejs/package.json`), +then use the `verify` lifecycle so test-resource setup and `failsafe:verify` +both run: ```sh cd src/sdk/java @@ -157,7 +154,9 @@ COPILOT_CLI_PATH=/dist-cli/prebuilds//copilot-runtime ./mvnw -pl sdk verify -Dit.test="MyFeatureIT#myTestMethod" -Dcopilot.cli.path="$COPILOT_CLI_PATH" ``` -Use `copilot-runtime.exe` for the wrapper name on Windows. +`` is Node's `-` (for example +`linux-x64`, `darwin-arm64`, `win32-x64`); use `copilot-runtime.exe` for the +wrapper name on Windows. In the standalone SDK repository, start at `java/` and use the pinned runtime: @@ -175,77 +174,15 @@ In the standalone SDK repository, start at `java/` and use the pinned runtime: | Replay proxy (TypeScript) | `test/harness/replayingCapiProxy.ts` | | Proxy server entry point | `test/harness/server.ts` | | Snapshot files | `test/snapshots//.yaml` | -| Existing IT tests for reference | `java/sdk/src/test/java/com/github/copilot/*IT.java` | - -## How the Proxy Matches Requests - -1. The proxy normalizes the incoming request's messages -2. It compares against each conversation in the YAML: - - System message matches if YAML has `${system}` (wildcard) - - User messages are compared by content (exact text match) - - Tool results are compared after normalizing `${workdir}` paths -3. If a match is found, the proxy returns the **next assistant message after the matched request prefix** -4. If no match, in CI mode (`GITHUB_ACTIONS=true`) it errors with "No cached response found" - -## YAML Format for Tool Calls - -If your test involves tool use: - -```yaml -conversations: - # First exchange: model wants to call a tool - - messages: - - role: system - content: ${system} - - role: user - content: Read the file test.txt - - role: assistant - content: I'll read that file. - tool_calls: - - id: toolcall_0 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - # Second exchange: after tool result is provided, model gives final answer - - messages: - - role: system - content: ${system} - - role: user - content: Read the file test.txt - - role: assistant - content: I'll read that file. - tool_calls: - - id: toolcall_0 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - - role: tool - tool_call_id: toolcall_0 - content: "1. Hello world!" - - role: assistant - content: The file test.txt contains "Hello world!" -``` +| Existing IT tests for reference | `java/sdk/src/test/java/com/github/copilot/**/*IT.java`; `SystemMessageSectionsIT` reuses Node.js suite captures, including a `view` tool call | + +## Tests that call built-in tools -**Important:** When the model calls tools like `view`, the CLI actually executes -them locally. The file must exist in the test's workDir. Create it in your test -before sending the prompt: +When the snapshot has the model call a built-in tool such as `view`, the CLI +really executes it in the test's workDir, and the next request carries its +result, which must match the snapshot's. Create any file the tool reads before +sending the prompt, with exactly the content the snapshot's tool result shows: ```java -Files.writeString(ctx.getWorkDir().resolve("test.txt"), "Hello world!\n"); +Files.writeString(ctx.getWorkDir().resolve("test.txt"), "Hello transform!"); ``` - -## Common Pitfalls - -1. **Prompt mismatch** — The user content in YAML must exactly match what - `session.sendAndWait(new MessageOptions().setPrompt("..."))` sends. -2. **Forgetting `${system}`** — Always use `${system}` for the system role content - unless testing a specific system message matching scenario. -3. **Tool execution** — If the snapshot has the model calling `view` or other - built-in tools, the CLI will actually execute those tools. Files must exist. -4. **Snapshot name parameter** — pass the explicit snapshot base name to - `configureForTest`, e.g., `configureForTest("category", "my_method_name")`. - Do not rely on camelCase-to-snake_case conversion. -5. **Cannot record via Java** — `CapiProxy.java` forces `GITHUB_ACTIONS=true`. - Always handcraft snapshots or use the Node.js proxy directly for recording. diff --git a/.github/skills/sdk-java-e2e-test/examples.md b/.github/skills/sdk-java-e2e-test/examples.md deleted file mode 100644 index 6cd98d1587..0000000000 --- a/.github/skills/sdk-java-e2e-test/examples.md +++ /dev/null @@ -1,181 +0,0 @@ -# Examples: New Java E2E Test with YAML Snapshot - -These examples document existing Java tests. For new E2E tests of shared -runtime behavior available through the SDK, use the TypeScript SDK suite -instead; add Java E2Es only for Java SDK surface area. - -## Example 1: Simple single-turn conversation (no tool calls) - -### Snapshot YAML - -File: `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml` - -```yaml -models: - - claude-sonnet-5 -conversations: - - messages: - - role: system - content: ${system} - - role: user - content: Who are you? - - role: assistant - content: >- - I'm Botanica, your helpful gardening assistant! I'm here to help you - with all things related to plants and gardening. Whether you have - questions about plant care, garden design, soil preparation, pest - management, or anything else in the world of gardening, I'm happy to - help. What would you like to know about plants or gardening today? -``` - -### Corresponding Java test method - -```java -@Test -void shouldUseReplacedIdentitySectionInResponse() throws Exception { - ctx.configureForTest("system_message_sections", "should_use_replaced_identity_section_in_response"); - - var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE) - .setSections(Map.of(SystemMessageSections.IDENTITY, - new SectionOverride().setAction(SectionOverrideAction.REPLACE) - .setContent("You are a helpful gardening assistant called Botanica. " - + "You only answer questions about plants and gardening."))); - - try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS); - - try { - AssistantMessageEvent response = session - .sendAndWait(new MessageOptions().setPrompt("Who are you?"), 60_000).get(90, TimeUnit.SECONDS); - - assertNotNull(response, "Expected a response from the assistant"); - String content = response.getData().content().toLowerCase(); - assertTrue(content.contains("botanica") || content.contains("garden") || content.contains("plant"), - "Expected response to reflect the replaced identity section, but got: " - + response.getData().content()); - } finally { - session.close(); - } - } -} -``` - -**Key points:** -- `configureForTest("system_message_sections", "should_use_replaced_identity_section_in_response")` - maps to `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml` -- The prompt `"Who are you?"` exactly matches the YAML's user content -- `ctx.createClient()` uses `fake-token-for-e2e-tests` — works in CI - ---- - -## Example 2: Multi-turn with tool calls (from existing tests) - -### Snapshot YAML - -File: `test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml` - -```yaml -models: - - claude-sonnet-5 -conversations: - # First exchange: model decides to call tools - - messages: - - role: system - content: ${system} - - role: user - content: Read the contents of test.txt and tell me what it says - - role: assistant - content: I'll read the test.txt file for you. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Reading test.txt file"}' - - id: toolcall_1 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - # Second exchange: after tool results come back, model gives final answer - - messages: - - role: system - content: ${system} - - role: user - content: Read the contents of test.txt and tell me what it says - - role: assistant - content: I'll read the test.txt file for you. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Reading test.txt file"}' - - id: toolcall_1 - type: function - function: - name: view - arguments: '{"path":"${workdir}/test.txt"}' - - role: tool - tool_call_id: toolcall_0 - content: Intent logged - - role: tool - tool_call_id: toolcall_1 - content: 1. Hello transform! - - role: assistant - content: |- - The file test.txt contains: - ``` - Hello transform! - ``` -``` - -### Corresponding Java test method - -```java -@Test -void transformOnIdentitySectionReceivesNonEmptyContent() throws Exception { - ctx.configureForTest("system_message_transform", "should_invoke_transform_callbacks_with_section_content"); - - ConcurrentHashMap capturedContent = new ConcurrentHashMap<>(); - - var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE) - .setSections(Map.of(SystemMessageSections.IDENTITY, new SectionOverride().setTransform(content -> { - capturedContent.put("identity", content); - return CompletableFuture.completedFuture(content); - }), SystemMessageSections.TONE, new SectionOverride().setTransform(content -> { - capturedContent.put("tone", content); - return CompletableFuture.completedFuture(content); - }))); - - try (CopilotClient client = ctx.createClient()) { - // Create the file the snapshot expects the CLI view tool to read - Path testFile = ctx.getWorkDir().resolve("test.txt"); - Files.writeString(testFile, "Hello transform!"); - - CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS); - - try { - AssistantMessageEvent response = session - .sendAndWait(new MessageOptions() - .setPrompt("Read the contents of test.txt and tell me what it says"), 60_000) - .get(90, TimeUnit.SECONDS); - - assertNotNull(response, "Expected a response from the assistant"); - - String identityContent = capturedContent.get("identity"); - assertNotNull(identityContent, "Expected identity transform callback to be invoked"); - assertTrue(!identityContent.isBlank(), "Expected identity section content to be non-empty"); - } finally { - session.close(); - } - } -} -``` - -**Key points:** -- The file `test.txt` must be created in `ctx.getWorkDir()` **before** sending the prompt -- The CLI's `view` tool will actually read that file; the YAML's tool result `"1. Hello transform!"` must match what `view` returns for that file content -- Two conversation entries: first for the tool-call decision, second for the final response after tool results diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml deleted file mode 100644 index d8ca560ab7..0000000000 --- a/.github/workflows/java-publish-maven.yml +++ /dev/null @@ -1,768 +0,0 @@ -name: "Java Publish to Maven Central" - -env: - HUSKY: 0 - -# This workflow is a read-only consumer of the commit being released. It builds -# every native classifier and the primary Java SDK artifact from a single -# immutable source SHA, injects the release version with -Drevision=, and -# publishes to Maven Central. It never commits to, tags, or otherwise mutates -# the repository. The cross-language `vX.Y.Z` GitHub Release and the -# `java/vX.Y.Z` traceability tag are created by .github/workflows/publish.yml -# only after publication succeeds. -# Only validated commits from main's history may be published. -# Keep dependency caches isolated from these independently selected sources. - -on: - workflow_dispatch: - inputs: - releaseVersion: - description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT" - required: false - type: string - sourceSha: - description: "Full commit SHA from main's history. Defaults to the triggering commit; dispatch this workflow from main." - required: false - type: string - prerelease: - description: "Is this a prerelease?" - type: boolean - required: false - default: false - workflow_call: - inputs: - releaseVersion: - description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT" - required: false - type: string - sourceSha: - description: "Full commit SHA from main's history. Defaults to the triggering commit; dispatch this workflow from main." - required: false - type: string - prerelease: - description: "Is this a prerelease?" - type: boolean - required: false - default: false - outputs: - mavenPublished: - description: "Whether the Java package was published to Maven Central" - value: ${{ jobs.deploy-maven.outputs.published }} - version: - description: "The published release version" - value: ${{ jobs.resolve-source.outputs.release_version }} - sourceSha: - description: "The immutable source commit that was published" - value: ${{ jobs.resolve-source.outputs.validated_source }} - secrets: - JAVA_MAVEN_CENTRAL_USERNAME: - required: true - JAVA_MAVEN_CENTRAL_PASSWORD: - required: true - JAVA_GPG_SECRET_KEY: - required: true - JAVA_GPG_PASSPHRASE: - required: true - -permissions: - contents: read - -concurrency: - group: publish-maven - cancel-in-progress: false - -jobs: - resolve-source: - name: Resolve immutable release source - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - shell: bash - working-directory: ./java - outputs: - validated_source: ${{ steps.source.outputs.validated_source }} - release_version: ${{ steps.versions.outputs.release_version }} - steps: - - name: Require a main-branch publication - working-directory: . - env: - WORKFLOW_REF: ${{ github.ref }} - run: | - if [ "$WORKFLOW_REF" != "refs/heads/main" ]; then - echo "::error::Java publication must be dispatched from main." - exit 1 - fi - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.sha }} - fetch-depth: 0 - persist-credentials: false - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: 22 - package-manager-cache: false - - - name: Validate and check out the release source - id: source - working-directory: . - env: - REQUESTED_SOURCE: ${{ inputs.sourceSha || github.sha }} - WORKFLOW_REF: ${{ github.ref }} - run: | - set -euo pipefail - VALIDATED_SOURCE=$(node java/scripts/resolve-release-source.mjs) - git checkout --detach "$VALIDATED_SOURCE" - echo "validated_source=$VALIDATED_SOURCE" >> "$GITHUB_OUTPUT" - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - - - name: Determine release version - id: versions - env: - REQUESTED_VERSION: ${{ inputs.releaseVersion }} - run: | - CURRENT_VERSION=$(mvn -N help:evaluate -Dexpression=project.version -q -DforceStdout) - echo "Current pom.xml version: $CURRENT_VERSION" - - if [ -n "$REQUESTED_VERSION" ]; then - RELEASE_VERSION="$REQUESTED_VERSION" - else - RELEASE_VERSION="${CURRENT_VERSION%-SNAPSHOT}" - fi - echo "Release version: $RELEASE_VERSION" - - 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." - exit 1 - fi - if [[ "$RELEASE_VERSION" == *-SNAPSHOT ]]; then - echo "::error::RELEASE_VERSION '$RELEASE_VERSION' must not be a SNAPSHOT." - exit 1 - fi - - echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" - - build-linux-arm64-classifier: - name: Build Linux ARM64 native classifier - needs: resolve-source - 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.resolve-source.outputs.validated_source }} - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: 22 - package-manager-cache: false - - - name: Build and validate linux-arm64 classifier - run: | - set -euo pipefail - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.outputs.release_version }}" - node copilot-native/scripts/validate-native-host.mjs linux-arm64 - mvn -B -pl copilot-native package -DskipTests -Drevision="$VERSION" -Dcopilot.native.libc=glibc - 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.resolve-source.outputs.release_version }}-linux-arm64.jar - java/copilot-native/target/linux-arm64-${{ needs.resolve-source.outputs.release_version }}.sha256 - if-no-files-found: error - retention-days: 1 - - build-linuxmusl-x64-classifier: - name: Build Linux musl x64 native classifier - needs: resolve-source - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ needs.resolve-source.outputs.validated_source }} - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: 22 - package-manager-cache: false - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - cache: "maven" - - - name: Build and validate linuxmusl-x64 classifier - run: | - set -euo pipefail - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.outputs.release_version }}" - docker run --rm \ - --volume "$GITHUB_WORKSPACE:/workspace" \ - --volume "$HOME/.m2:/root/.m2" \ - --workdir /workspace/java \ - --env HOST_GID="$(id -g)" \ - --env HOST_UID="$(id -u)" \ - "eclipse-temurin:25-jdk-alpine" \ - sh -c "apk add --no-cache git java-cacerts maven nodejs npm && - export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts && - git config --global --add safe.directory /workspace && - node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 && - mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl -Drevision=$VERSION && - chown -R \$HOST_UID:\$HOST_GID copilot-native/target" - JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" - PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" - test -f "$JAR" - node copilot-native/scripts/validate-native-artifact.mjs \ - classifier linuxmusl-x64 "$JAR" "$(basename "$JAR")" .. - node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" - MANIFEST="copilot-native/target/linuxmusl-x64-$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-linuxmusl-x64-release-${{ github.run_id }}-${{ github.run_attempt }} - path: | - java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.resolve-source.outputs.release_version }}-linuxmusl-x64.jar - java/copilot-native/target/linuxmusl-x64-${{ needs.resolve-source.outputs.release_version }}.sha256 - if-no-files-found: error - retention-days: 1 - - build-windows-x64-classifier: - name: Build Windows x64 native classifier - needs: resolve-source - 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.resolve-source.outputs.validated_source }} - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: 22 - package-manager-cache: false - - - name: Build and validate win32-x64 classifier - run: | - $sourceCommit = git rev-parse HEAD - if ($sourceCommit -ne '${{ needs.resolve-source.outputs.validated_source }}') { - throw "Checked out $sourceCommit instead of the resolved release source." - } - $version = '${{ needs.resolve-source.outputs.release_version }}' - node copilot-native/scripts/validate-native-host.mjs win32-x64 - mvn -B -pl copilot-native package -DskipTests "-Drevision=$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.resolve-source.outputs.release_version }}-win32-x64.jar - java/copilot-native/target/win32-x64-${{ needs.resolve-source.outputs.release_version }}.sha256 - if-no-files-found: error - retention-days: 1 - - build-windows-arm64-classifier: - name: Build Windows ARM64 native classifier - needs: resolve-source - 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.resolve-source.outputs.validated_source }} - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: 22 - package-manager-cache: false - - - name: Build and validate win32-arm64 classifier - run: | - $sourceCommit = git rev-parse HEAD - if ($sourceCommit -ne '${{ needs.resolve-source.outputs.validated_source }}') { - throw "Checked out $sourceCommit instead of the resolved release source." - } - $version = '${{ needs.resolve-source.outputs.release_version }}' - node copilot-native/scripts/validate-native-host.mjs win32-arm64 - mvn -B -pl copilot-native package -DskipTests "-Drevision=$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.resolve-source.outputs.release_version }}-win32-arm64.jar - java/copilot-native/target/win32-arm64-${{ needs.resolve-source.outputs.release_version }}.sha256 - if-no-files-found: error - retention-days: 1 - - build-darwin-classifier: - name: Build Darwin native classifier - needs: resolve-source - 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.resolve-source.outputs.validated_source }} - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: 22 - package-manager-cache: false - - - name: Build and validate darwin-arm64 classifier - run: | - set -euo pipefail - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.outputs.release_version }}" - node copilot-native/scripts/validate-native-host.mjs darwin-arm64 - mvn -B -pl copilot-native package -DskipTests -Drevision="$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.resolve-source.outputs.release_version }}-darwin-arm64.jar - java/copilot-native/target/darwin-arm64-${{ needs.resolve-source.outputs.release_version }}.sha256 - if-no-files-found: error - retention-days: 1 - - build-darwin-x64-classifier: - name: Build Darwin x64 native classifier - needs: resolve-source - runs-on: macos-15-intel - permissions: - contents: read - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ needs.resolve-source.outputs.validated_source }} - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - with: - node-version: 22 - package-manager-cache: false - - - name: Build and validate darwin-x64 classifier - run: | - set -euo pipefail - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.outputs.release_version }}" - node copilot-native/scripts/validate-native-host.mjs darwin-x64 - mvn -B -pl copilot-native package -DskipTests -Drevision="$VERSION" - JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-darwin-x64.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-x64 "$JAR" "$(basename "$JAR")" .. - node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" - MANIFEST="copilot-native/target/darwin-x64-$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-x64-release-${{ github.run_id }}-${{ github.run_attempt }} - path: | - java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.resolve-source.outputs.release_version }}-darwin-x64.jar - java/copilot-native/target/darwin-x64-${{ needs.resolve-source.outputs.release_version }}.sha256 - if-no-files-found: error - retention-days: 1 - - deploy-maven: - name: Deploy Java release to Maven Central - needs: - [ - resolve-source, - build-linux-arm64-classifier, - build-linuxmusl-x64-classifier, - build-windows-x64-classifier, - build-windows-arm64-classifier, - build-darwin-classifier, - build-darwin-x64-classifier, - ] - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - shell: bash - working-directory: ./java - outputs: - version: ${{ needs.resolve-source.outputs.release_version }} - published: ${{ steps.publish-maven.outcome == 'success' }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ needs.resolve-source.outputs.validated_source }} - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - 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 - package-manager-cache: false - - - 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-linuxmusl-x64-release-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/java-native-linuxmusl-x64 - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: java-native-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 - - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: java-native-darwin-x64-release-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/java-native-darwin-x64 - - - name: Verify immutable source and Linux ARM64 classifier - id: linux-arm64-artifact - run: | - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.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: | - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.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 Linux musl x64 classifier - id: linuxmusl-x64-artifact - run: | - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.outputs.release_version }}" - ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linuxmusl-x64" - JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" - MANIFEST="$ARTIFACT_DIRECTORY/linuxmusl-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 linuxmusl-x64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" - echo "linuxmusl_x64_jar=$JAR" >> "$GITHUB_OUTPUT" - echo "linuxmusl_x64_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.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.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.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.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: Verify immutable source and Darwin x64 classifier - id: darwin-x64-artifact - run: | - SOURCE_COMMIT=$(git rev-parse HEAD) - if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then - echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." - exit 1 - fi - VERSION="${{ needs.resolve-source.outputs.release_version }}" - ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-darwin-x64" - JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-darwin-x64.jar" - MANIFEST="$ARTIFACT_DIRECTORY/darwin-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 darwin-x64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" - echo "darwin_x64_jar=$JAR" >> "$GITHUB_OUTPUT" - echo "darwin_x64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" - - - name: Build Linux classifier and deploy complete release - id: publish-maven - run: | - VERSION="${{ needs.resolve-source.outputs.release_version }}" - mvn -B deploy -DskipTests -DskipITs -Prelease -Drevision="$VERSION" -Dcopilot.native.libc=glibc \ - "-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ - "-Dcopilot.native.external.linuxmusl.x64.classifier.path=${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}" \ - "-Dcopilot.native.external.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 }}" \ - "-Dcopilot.native.external.darwin.x64.classifier.path=${{ steps.darwin-x64-artifact.outputs.darwin_x64_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 -Drevision="$VERSION" -DforceStdout) - ARTIFACT_ID=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.artifactId -Drevision="$VERSION" -DforceStdout) - POM_VERSION=$(mvn -q -pl copilot-native help:evaluate -Dexpression=project.version -Drevision="$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 commit:** \`${{ needs.resolve-source.outputs.validated_source }}\`" - 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 "| \`linuxmusl-x64\` | \`Alpine x64\` | \`$(basename "${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}")\` | \`${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_sha }}\` | Published |" - echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" - echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" - echo "| \`darwin-x64\` | \`macos-15-intel\` | \`$(basename "${{ steps.darwin-x64-artifact.outputs.darwin_x64_jar }}")\` | \`${{ steps.darwin-x64-artifact.outputs.darwin_x64_sha }}\` | Published |" - 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 }} diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml index ce5b8c5f4e..77d5287964 100644 --- a/.github/workflows/java-publish-snapshot.yml +++ b/.github/workflows/java-publish-snapshot.yml @@ -171,6 +171,80 @@ jobs: if-no-files-found: error retention-days: 1 + build-linuxmusl-arm64-classifier: + name: Build Linux musl ARM64 snapshot classifier + needs: resolve-source + runs-on: ubuntu-24.04-arm + permissions: + contents: read + outputs: + version: ${{ steps.build.outputs.version }} + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "25" + distribution: "microsoft" + cache: "maven" + + - name: Build and validate linuxmusl-arm64 classifier + id: build + run: | + set -euo pipefail + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --volume "$HOME/.m2:/root/.m2" \ + --workdir /workspace/java \ + --env HOST_GID="$(id -g)" \ + --env HOST_UID="$(id -u)" \ + "eclipse-temurin:25-jdk-alpine" \ + sh -c "apk add --no-cache git java-cacerts maven nodejs npm && + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts && + git config --global --add safe.directory /workspace && + node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64 && + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl && + chown -R \$HOST_UID:\$HOST_GID copilot-native/target" + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar" + PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" + test -f "$JAR" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linuxmusl-arm64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linuxmusl-arm64.jar + java/copilot-native/target/linuxmusl-arm64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + retention-days: 1 + build-windows-x64-classifier: name: Build Windows x64 snapshot classifier needs: resolve-source @@ -426,6 +500,7 @@ jobs: resolve-source, build-linux-arm64-classifier, build-linuxmusl-x64-classifier, + build-linuxmusl-arm64-classifier, build-windows-x64-classifier, build-windows-arm64-classifier, build-darwin-classifier, @@ -468,6 +543,11 @@ jobs: name: java-native-linuxmusl-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/java-native-linuxmusl-x64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-linuxmusl-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-linuxmusl-arm64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: java-native-win32-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} @@ -569,6 +649,31 @@ jobs: echo "linuxmusl_x64_jar=$JAR" >> "$GITHUB_OUTPUT" echo "linuxmusl_x64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Linux musl ARM64 classifier + id: linuxmusl-arm64-artifact + run: | + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION="${{ steps.linux-arm64-artifact.outputs.version }}" + if [ "$VERSION" != "${{ needs.build-linuxmusl-arm64-classifier.outputs.version }}" ]; then + echo "::error::Linux musl ARM64 classifier version does not match deploy version." + exit 1 + fi + ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linuxmusl-arm64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-arm64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/linuxmusl-arm64-$VERSION.sha256" + test -f "$JAR" + test -f "$MANIFEST" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + classifier linuxmusl-arm64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "linuxmusl_arm64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "linuxmusl_arm64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Darwin classifier id: darwin-artifact run: | @@ -650,6 +755,7 @@ jobs: mvn -B deploy -DskipTests -DskipITs -Dcopilot.native.libc=glibc \ "-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ "-Dcopilot.native.external.linuxmusl.x64.classifier.path=${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}" \ + "-Dcopilot.native.external.linuxmusl.arm64.classifier.path=${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}" \ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" \ @@ -689,6 +795,7 @@ jobs: echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |" echo "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |" echo "| \`linuxmusl-x64\` | \`Alpine x64\` | \`$(basename "${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}")\` | \`${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_sha }}\` | Published |" + echo "| \`linuxmusl-arm64\` | \`Alpine ARM64\` | \`$(basename "${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_jar }}")\` | \`${{ steps.linuxmusl-arm64-artifact.outputs.linuxmusl_arm64_sha }}\` | Published |" echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" echo "| \`darwin-x64\` | \`macos-15-intel\` | \`$(basename "${{ steps.darwin-x64-artifact.outputs.darwin_x64_jar }}")\` | \`${{ steps.darwin-x64-artifact.outputs.darwin_x64_sha }}\` | Published |" diff --git a/.github/workflows/java-smoke-test.yml b/.github/workflows/java-smoke-test.yml deleted file mode 100644 index 95f662beb1..0000000000 --- a/.github/workflows/java-smoke-test.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: "Java smoke test" - -on: - workflow_dispatch: - workflow_call: - secrets: - COPILOT_GITHUB_TOKEN: - required: true - -permissions: - contents: read - -jobs: - smoke-test-jdk17: - name: Build SDK and run smoke test (JDK 17) - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 17 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "17" - distribution: "microsoft" - cache: "maven" - - - uses: ./.github/actions/setup-copilot - id: setup-copilot - - - name: Build SDK and install to local repo - run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install - - - name: Create and run smoke test via Copilot CLI - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} - run: | - cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' - You are running inside the copilot-sdk monorepo, in the java/ subdirectory. - The SDK has already been built and installed into the local Maven repository. - JDK 17 and Maven are already installed and on PATH. - - Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: - - **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. - - **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. - - Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). - - If any step fails, exit with a non-zero exit code. Do not silently fix errors. - PROMPT_EOF - - node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - - - name: Run smoke test jar - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - run: | - cd smoke-test - java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar - echo "Smoke test passed (exit code 0)" - - smoke-test-java25: - name: Build SDK and run smoke test (JDK 25) - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - defaults: - run: - shell: bash - working-directory: ./java - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 - with: - java-version: "25" - distribution: "microsoft" - cache: "maven" - - - uses: ./.github/actions/setup-copilot - id: setup-copilot - - - name: Build SDK and install to local repo - run: mvn -DskipTests -DskipITs -Pskip-test-harness clean install - - - name: Create and run smoke test via Copilot CLI - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} - run: | - cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' - You are running inside the copilot-sdk monorepo, in the java/ subdirectory. - The SDK has already been built and installed into the local Maven repository. - JDK 25 and Maven are already installed and on PATH. - - Execute the prompt at `sdk/src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: - - **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. - - **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. - - **Critical override — enable Virtual Threads for JDK 25:** After creating the Java source file from the README "Quick Start" section but BEFORE building, you must modify the source file to enable virtual thread support. The Quick Start code contains inline comments that start with `// JDK 25+:` — these are instructions. Find every such comment and follow what it says (comment out lines it says to comment out, uncomment lines it says to uncomment). Add any imports required by the newly uncommented code (e.g. `java.util.concurrent.Executors`). - Also set `maven.compiler.source` and `maven.compiler.target` to `25` in the `pom.xml`. - - Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, apply the JDK 25 virtual thread modifications described above, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). - - If any step fails, exit with a non-zero exit code. Do not silently fix errors. - PROMPT_EOF - - node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - - - name: Run smoke test jar - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - run: | - cd smoke-test - java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar - echo "Smoke test passed (exit code 0)" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 6a13c57ee2..0000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,1069 +0,0 @@ -name: Publish SDK packages - -env: - HUSKY: 0 - -on: - workflow_dispatch: - inputs: - dist-tag: - description: "Tag to publish under" - type: choice - required: true - default: "prerelease" - options: - - latest - - prerelease - - unstable - - canary - version: - description: "Version override (optional, e.g., 1.0.0). If empty, auto-increments. Unstable overrides must use -unstable." - type: string - required: false - mode: - description: "Publish or validate without registry and release mutations" - type: choice - required: true - default: publish - options: - - publish - - dry-run - runtime: - description: "Runtime metadata (automation only)" - type: string - required: false - test-policy: - description: "Runtime E2E test policy" - type: choice - required: true - default: required - options: - - required - - advisory - - skipped - -permissions: - contents: read - -concurrency: - group: ${{ inputs.mode == 'dry-run' && format('publish-dry-run-{0}', github.run_id) || inputs.runtime != '' && format('publish-runtime-{0}', github.run_id) || inputs.dist-tag == 'unstable' && 'sdk-runtime-public-unstable' || 'publish' }} - cancel-in-progress: false - -jobs: - validate-dispatch: - name: Validate dispatch - runs-on: ubuntu-latest - outputs: - kind: ${{ steps.validate.outputs.kind }} - runtime_run_id: ${{ steps.validate.outputs.runtime_run_id }} - runtime_sha: ${{ steps.validate.outputs.runtime_sha }} - runtime_version: ${{ steps.validate.outputs.runtime_version }} - test_policy: ${{ steps.validate.outputs.test_policy }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Validate release dispatch - id: validate - working-directory: ./nodejs - env: - DIST_TAG: ${{ inputs.dist-tag }} - MODE: ${{ inputs.mode }} - RUNTIME_JSON: ${{ inputs.runtime }} - TEST_POLICY: ${{ inputs.test-policy }} - VERSION_OVERRIDE: ${{ inputs.version }} - run: npx tsx scripts/runtime-release-identity.ts - - # Shared job to calculate version once for all publish jobs - version: - name: Calculate Version - needs: validate-dispatch - if: needs.validate-dispatch.outputs.kind == 'direct' - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - outputs: - version: ${{ steps.unstable_version.outputs.VERSION || steps.version.outputs.VERSION }} - current: ${{ steps.version.outputs.CURRENT }} - current-prerelease: ${{ steps.version.outputs.CURRENT_PRERELEASE }} - defaults: - run: - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: ${{ inputs.dist-tag == 'unstable' && '0' || '1' }} - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - run: npm ci --ignore-scripts - - name: Plan unstable version - if: inputs.dist-tag == 'unstable' - id: unstable_version - env: - GH_TOKEN: ${{ github.token }} - SDK_CHANNEL: unstable - SDK_SHA: ${{ github.sha }} - SDK_VERSION_OVERRIDE: ${{ inputs.version }} - WORKFLOW_RUN_ID: ${{ github.run_id }} - run: | - set -euo pipefail - WORKFLOW_CREATED_AT="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq .created_at)" - gh api --paginate "/repos/$GITHUB_REPOSITORY/releases?per_page=100" | - jq -s 'add' > "$RUNNER_TEMP/sdk-releases.json" - export SDK_RELEASES_FILE="$RUNNER_TEMP/sdk-releases.json" - export WORKFLOW_CREATED_AT - VERSION="$(npx tsx scripts/unstable-version.ts)" - npm exec -- semver "$VERSION" >/dev/null - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "Planned unstable version: $VERSION" >> "$GITHUB_STEP_SUMMARY" - - name: Get version - if: inputs.dist-tag != 'unstable' - id: version - run: | - CURRENT="$(node scripts/get-version.js current)" - echo "CURRENT=$CURRENT" >> $GITHUB_OUTPUT - echo "Current latest version: $CURRENT" >> $GITHUB_STEP_SUMMARY - CURRENT_PRERELEASE="$(node scripts/get-version.js current-prerelease)" - echo "CURRENT_PRERELEASE=$CURRENT_PRERELEASE" >> $GITHUB_OUTPUT - echo "Current prerelease version: $CURRENT_PRERELEASE" >> $GITHUB_STEP_SUMMARY - if [ -n "${{ github.event.inputs.version }}" ]; then - VERSION="${{ github.event.inputs.version }}" - # Validate version format matches dist-tag - if [ "${{ github.event.inputs.dist-tag }}" = "latest" ]; then - if [[ "$VERSION" == *-* ]]; then - echo "❌ Error: Version '$VERSION' has a prerelease suffix but dist-tag is 'latest'" >> $GITHUB_STEP_SUMMARY - echo "Use a version without suffix (e.g., '1.0.0') for latest releases" - exit 1 - fi - else - if [[ "$VERSION" != *-* ]]; then - echo "❌ Error: Version '$VERSION' has no prerelease suffix but dist-tag is '${{ github.event.inputs.dist-tag }}'" >> $GITHUB_STEP_SUMMARY - echo "Use a version with suffix (e.g., '1.0.0-preview.0') for prerelease" - exit 1 - fi - fi - echo "Using manual version override: $VERSION" >> $GITHUB_STEP_SUMMARY - else - VERSION="$(node scripts/get-version.js ${{ github.event.inputs.dist-tag }})" - echo "Auto-incremented version: $VERSION" >> $GITHUB_STEP_SUMMARY - fi - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - name: Verify version is available on public npm - if: inputs.dist-tag != 'unstable' - env: - VERSION: ${{ steps.version.outputs.VERSION }} - run: | - node scripts/npm-release.js preflight \ - @github/copilot-sdk \ - "$VERSION" \ - https://registry.npmjs.org - - package-nodejs: - name: Package Node.js SDK - needs: version - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - run: npm ci --ignore-scripts - - name: Set version - run: node scripts/set-version.js - env: - VERSION: ${{ needs.version.outputs.version }} - - name: Build - run: npm run build - - name: Pack - run: | - npm run pack:release - TARBALL_COUNT="$(find . -maxdepth 1 -name 'github-copilot-sdk-*.tgz' | wc -l | tr -d ' ')" - if [ "$TARBALL_COUNT" -ne 9 ]; then - echo "::error::Expected nine Node.js package tarballs, found $TARBALL_COUNT." - exit 1 - fi - npm run verify:release-packages - - name: Create unstable package manifest - if: inputs.dist-tag == 'unstable' - env: - SDK_VERSION: ${{ needs.version.outputs.version }} - run: npm run release:manifest -- create-package-set package-set-manifest.json . - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: nodejs-package - path: | - nodejs/github-copilot-sdk-*.tgz - nodejs/package-set-manifest.json - if-no-files-found: error - - publish-nodejs: - name: Publish Node.js SDK - needs: package-nodejs - if: inputs.mode == 'publish' && (github.ref == 'refs/heads/main' || inputs.dist-tag == 'unstable') - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - id-token: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - name: Install release dependencies - if: inputs.dist-tag == 'unstable' - working-directory: ./nodejs - run: npm ci --ignore-scripts - - name: Update npm for OIDC support - run: npm i -g "npm@11.6.3" - - name: Download Node.js package - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: nodejs-package - path: ./dist - - name: Publish tarball to public npm - env: - DIST_TAG: ${{ github.event.inputs.dist-tag }} - run: | - set -euo pipefail - if [ "$DIST_TAG" = "unstable" ]; then - node nodejs/scripts/npm-release.js publish-manifest \ - dist/package-set-manifest.json dist unstable https://registry.npmjs.org public - exit 0 - fi - shopt -s nullglob - TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 9 ]; then - echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." - exit 1 - fi - MAIN_TARBALL="" - for TARBALL in "${TARBALLS[@]}"; do - PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" - if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then - MAIN_TARBALL="$TARBALL" - continue - fi - node nodejs/scripts/npm-release.js publish \ - "$TARBALL" \ - "$DIST_TAG" \ - https://registry.npmjs.org \ - public - done - if [ -z "$MAIN_TARBALL" ]; then - echo "::error::Main @github/copilot-sdk tarball not found." - exit 1 - fi - node nodejs/scripts/npm-release.js publish \ - "$MAIN_TARBALL" \ - "$DIST_TAG" \ - https://registry.npmjs.org \ - public - - publish-nodejs-internal: - name: Publish Node.js SDK to internal feed - needs: publish-nodejs - environment: cicd - runs-on: ubuntu-latest - concurrency: - group: sdk-runtime-internal-${{ inputs.dist-tag }} - cancel-in-progress: false - queue: max - permissions: - actions: read - contents: read - id-token: write - env: - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - name: Install release dependencies - if: inputs.dist-tag == 'unstable' - working-directory: ./nodejs - run: npm ci --ignore-scripts - - name: Download Node.js package - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: nodejs-package - path: ./dist - - name: Azure Login (OIDC -> id-cpd-ci) - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - name: Configure feed auth - run: | - set -euo pipefail - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - printf '%s\n' \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > "$HOME/.npmrc" - - name: Publish tarball to internal feed - env: - DIST_TAG: ${{ github.event.inputs.dist-tag }} - run: | - set -euo pipefail - if [ "$FEED_URL" != "https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/" ]; then - echo "::error::FEED_URL ('$FEED_URL') is not the expected internal feed. Refusing to publish." - exit 1 - fi - if [ "$DIST_TAG" = "unstable" ]; then - node nodejs/scripts/npm-release.js publish-manifest \ - dist/package-set-manifest.json dist unstable "$FEED_URL" azure - exit 0 - fi - shopt -s nullglob - TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 9 ]; then - echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." - exit 1 - fi - MAIN_TARBALL="" - for TARBALL in "${TARBALLS[@]}"; do - PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" - if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then - MAIN_TARBALL="$TARBALL" - continue - fi - node nodejs/scripts/npm-release.js publish \ - "$TARBALL" \ - "$DIST_TAG" \ - "$FEED_URL" \ - azure - done - if [ -z "$MAIN_TARBALL" ]; then - echo "::error::Main @github/copilot-sdk tarball not found." - exit 1 - fi - node nodejs/scripts/npm-release.js publish \ - "$MAIN_TARBALL" \ - "$DIST_TAG" \ - "$FEED_URL" \ - azure - - publish-dotnet: - name: Publish .NET SDK - if: inputs.dist-tag != 'unstable' - needs: version - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - defaults: - run: - working-directory: ./dotnet - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - dotnet-version: "10.0.x" - - name: Restore dependencies - run: dotnet restore - - name: Build and pack - run: dotnet pack src/GitHub.Copilot.SDK.csproj -c Release -p:Version=${{ needs.version.outputs.version }} -o ./artifacts - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: dotnet-package - path: | - dotnet/artifacts/*.nupkg - dotnet/artifacts/*.snupkg - - name: NuGet login (OIDC) - if: github.ref == 'refs/heads/main' - uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 - id: nuget-login - with: - # The following must be a username, not an organization name, and that user must have configured Trusted Publishing - # for this owner/repo/workflow combination in their NuGet.org account settings. We could set up a dedicated user for - # this purpose if needed, but then we'd have to manage that account separately. Other GitHub-owned packages on NuGet - # are associated with individual maintainers' accounts too. - user: stevesanderson - - name: Publish to NuGet - if: github.ref == 'refs/heads/main' - run: | - dotnet nuget push ./artifacts/*.nupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate --no-symbols - dotnet nuget push ./artifacts/*.snupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate - - publish-dotnet-internal: - name: Publish .NET SDK to internal feed - needs: publish-dotnet - if: github.ref == 'refs/heads/main' - environment: cicd - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - id-token: write - env: - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/nuget/v3/index.json - steps: - - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - dotnet-version: "10.0.x" - - name: Download .NET package - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: dotnet-package - path: ./dist - - name: Azure Login (OIDC -> id-cpd-ci) - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - name: Publish package to internal feed - run: | - set -euo pipefail - if [ "$FEED_URL" != "https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/nuget/v3/index.json" ]; then - echo "::error::FEED_URL ('$FEED_URL') is not the expected internal feed. Refusing to publish." - exit 1 - fi - shopt -s nullglob - PACKAGES=(./dist/*.nupkg) - if [ "${#PACKAGES[@]}" -ne 1 ]; then - echo "::error::Expected one .NET package, found ${#PACKAGES[@]}." - exit 1 - fi - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - dotnet nuget add source "$FEED_URL" --name CopilotInternal - # Keep the short-lived token out of NuGet.Config and command-line arguments. - export NuGetPackageSourceCredentials_CopilotInternal="Username=azure;Password=$TOKEN;ValidAuthenticationTypes=Basic" - # Azure Artifacts does not support .snupkg symbol packages. - dotnet nuget push "${PACKAGES[0]}" \ - --api-key AzureArtifacts \ - --source CopilotInternal \ - --skip-duplicate \ - --no-symbols - - publish-rust: - name: Publish Rust SDK - if: inputs.dist-tag != 'unstable' - needs: version - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./rust - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable - with: - toolchain: "1.94.0" - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - workspaces: "rust" - - name: Set version - run: sed -i -E 's/^version = ".*"$/version = "${{ needs.version.outputs.version }}"/' Cargo.toml - - name: Snapshot CLI version + hashes for build.rs - run: | - bash scripts/snapshot-bundled-cli-version.sh - bash scripts/snapshot-bundled-in-process-version.sh - - name: Verify CLI version snapshots exist - run: | - for snapshot in cli-version.txt cli-version-in-process.txt; do - if [[ ! -f "${snapshot}" ]]; then - echo "::error::${snapshot} was not generated. The Snapshot step must run before packaging." - exit 1 - fi - done - - name: Package (dry run) - run: cargo publish --dry-run --allow-dirty - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: rust-package - path: rust/target/package/*.crate - - name: Publish to crates.io - if: github.ref == 'refs/heads/main' - run: cargo publish --allow-dirty - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - - publish-python: - name: Publish Python SDK - if: inputs.dist-tag != 'unstable' - needs: version - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - defaults: - run: - working-directory: ./python - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 - with: - python-version: "3.12" - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: "22.x" - - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - - name: Set version - run: sed -i "s/^version = .*/version = \"${{ needs.version.outputs.version }}\"/" pyproject.toml - - name: Inject CLI version - run: node scripts/inject-cli-version.mjs - - name: Build wheel - run: uv build --wheel --out-dir dist - - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: python-package - path: python/dist/* - - name: Publish to PyPI - if: github.ref == 'refs/heads/main' - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - with: - packages-dir: python/dist/ - - publish-java: - name: Publish Java SDK - if: inputs.dist-tag != 'unstable' && github.ref == 'refs/heads/main' - needs: version - permissions: - contents: read - uses: ./.github/workflows/java-publish-maven.yml - with: - releaseVersion: ${{ needs.version.outputs.version }} - sourceSha: ${{ github.sha }} - prerelease: ${{ github.event.inputs.dist-tag == 'prerelease' }} - secrets: inherit - - github-release: - name: Create GitHub Release - needs: - [ - version, - publish-nodejs, - publish-dotnet, - publish-python, - publish-rust, - publish-java, - ] - if: | - always() && - github.ref == 'refs/heads/main' && - inputs.dist-tag != 'unstable' && - needs.version.result == 'success' && - needs.publish-nodejs.result == 'success' && - needs.publish-dotnet.result == 'success' && - needs.publish-python.result == 'success' && - needs.publish-rust.result == 'success' && - needs.publish-java.outputs.mavenPublished == 'true' - runs-on: ubuntu-latest - permissions: - actions: write - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Create GitHub Release - if: github.event.inputs.dist-tag == 'latest' - run: | - NOTES_FLAG="" - if git rev-parse "v${{ needs.version.outputs.current }}" >/dev/null 2>&1; then - NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current }}" - fi - gh release create "v${{ needs.version.outputs.version }}" \ - --title "v${{ needs.version.outputs.version }}" \ - --generate-notes $NOTES_FLAG \ - --target ${{ github.sha }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create GitHub Pre-Release - if: github.event.inputs.dist-tag == 'prerelease' - run: | - NOTES_FLAG="" - if git rev-parse "v${{ needs.version.outputs.current-prerelease }}" >/dev/null 2>&1; then - NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current-prerelease }}" - fi - gh release create "v${{ needs.version.outputs.version }}" \ - --prerelease \ - --title "v${{ needs.version.outputs.version }}" \ - --generate-notes $NOTES_FLAG \ - --target ${{ github.sha }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Trigger changelog generation - run: gh workflow run release-changelog.lock.yml -f tag="v${{ needs.version.outputs.version }}" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Tag Go SDK submodule - if: github.event.inputs.dist-tag == 'latest' || github.event.inputs.dist-tag == 'prerelease' - run: | - set -e - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git fetch --tags - TAG_NAME="go/v${{ needs.version.outputs.version }}" - # Try to create the tag - will fail if it already exists - if git tag "$TAG_NAME" ${{ github.sha }} 2>/dev/null; then - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git "$TAG_NAME" - echo "Created and pushed tag $TAG_NAME" - else - echo "Tag $TAG_NAME already exists, skipping" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Tag Rust SDK - # Keep a language-scoped source tag for traceability. Rust is - # included in the cross-language `vX.Y.Z` GitHub Release. - if: github.event.inputs.dist-tag == 'latest' || github.event.inputs.dist-tag == 'prerelease' - run: | - set -e - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git fetch --tags - VERSION="${{ needs.version.outputs.version }}" - TAG_NAME="rust/v${VERSION}" - if git tag "$TAG_NAME" ${{ github.sha }} 2>/dev/null; then - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git "$TAG_NAME" - echo "Created and pushed tag $TAG_NAME" - else - echo "Tag $TAG_NAME already exists, skipping tag push" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - tag-java: - name: Tag Java SDK - needs: [version, publish-java, github-release] - if: | - success() && - (github.event.inputs.dist-tag == 'latest' || - github.event.inputs.dist-tag == 'prerelease') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ needs.publish-java.outputs.sourceSha }} - fetch-depth: 0 - - - name: Tag Java SDK - # Reuse a tag only when it identifies the source that was published. - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git fetch --tags origin - TAG_NAME="java/v${VERSION}" - if git show-ref --verify --quiet "refs/tags/$TAG_NAME"; then - TAG_COMMIT=$(git rev-parse --verify "refs/tags/${TAG_NAME}^{commit}") - if [ "$TAG_COMMIT" != "$SOURCE_SHA" ]; then - echo "::error::Tag $TAG_NAME points to $TAG_COMMIT, expected $SOURCE_SHA. Refusing to overwrite it." - exit 1 - fi - echo "Tag $TAG_NAME already points to the release source, skipping tag push" - else - STATUS=$? - if [ "$STATUS" -ne 1 ]; then - echo "::error::Could not inspect tag $TAG_NAME." - exit "$STATUS" - fi - git tag "$TAG_NAME" "$SOURCE_SHA" - git push origin "refs/tags/$TAG_NAME" - echo "Created and pushed tag $TAG_NAME" - fi - env: - VERSION: ${{ needs.version.outputs.version }} - SOURCE_SHA: ${{ needs.publish-java.outputs.sourceSha }} - - deploy-java-site: - name: Deploy Java documentation site - needs: [version, tag-java] - runs-on: ubuntu-latest - permissions: {} - steps: - - name: Trigger Java documentation site deploy - # A failed dispatch can be retried without recreating the GitHub release. - run: | - set -euo pipefail - TAG="java/v${VERSION}" - PUBLISH_AS_LATEST=true - if [ "$DIST_TAG" = "prerelease" ]; then - PUBLISH_AS_LATEST=false - fi - echo "Triggering site deployment for version ${VERSION} (tag: ${TAG})" - gh workflow run deploy-site.yml \ - --repo github/copilot-sdk-java \ - -f version="${VERSION}" \ - -f publish_as_latest="${PUBLISH_AS_LATEST}" \ - -f monorepo_tag="${TAG}" - env: - VERSION: ${{ needs.version.outputs.version }} - DIST_TAG: ${{ github.event.inputs.dist-tag }} - GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_GITHUB_TOKEN }} - runtime-plan: - name: Plan runtime release - needs: validate-dispatch - if: needs.validate-dispatch.outputs.kind == 'runtime' - runs-on: ubuntu-latest - environment: cicd - permissions: - actions: read - contents: read - outputs: - artifact_name: ${{ steps.plan.outputs.artifact_name }} - sdk_version: ${{ steps.plan.outputs.sdk_version }} - workflow_created_at: ${{ steps.plan.outputs.workflow_created_at }} - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Calculate the release identity - id: plan - working-directory: ./nodejs - env: - CHANNEL: ${{ inputs.dist-tag }} - GH_TOKEN: ${{ github.token }} - SDK_CHANNEL: ${{ inputs.dist-tag }} - SDK_SHA: ${{ github.sha }} - WORKFLOW_RUN_ID: ${{ github.run_id }} - WORKFLOW_RUN_NUMBER: ${{ github.run_number }} - run: | - set -euo pipefail - WORKFLOW_CREATED_AT="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq .created_at)" - gh api --paginate "/repos/$GITHUB_REPOSITORY/releases?per_page=100" | - jq -s 'add' > "$RUNNER_TEMP/sdk-releases.json" - export SDK_RELEASES_FILE="$RUNNER_TEMP/sdk-releases.json" - export WORKFLOW_CREATED_AT - SDK_VERSION="$(npx tsx scripts/unstable-version.ts)" - npm exec -- semver "$SDK_VERSION" >/dev/null - ARTIFACT_NAME="nodejs-${CHANNEL}-${SDK_VERSION}" - echo "Runtime E2E test policy: ${{ needs.validate-dispatch.outputs.test_policy }}" >> "$GITHUB_STEP_SUMMARY" - { - echo "artifact_name=$ARTIFACT_NAME" - echo "sdk_version=$SDK_VERSION" - echo "workflow_created_at=$WORKFLOW_CREATED_AT" - } >> "$GITHUB_OUTPUT" - - runtime-acquire: - name: Acquire runtime - needs: [validate-dispatch, runtime-plan] - runs-on: ubuntu-latest - environment: cicd - permissions: - contents: read - packages: read - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - - name: Configure authentication-only GitHub Packages access - env: - NODE_AUTH_TOKEN: ${{ github.token }} - run: echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > "$HOME/.npmrc" - - name: Download and validate all runtime platforms - env: - NODE_AUTH_TOKEN: ${{ github.token }} - RUNTIME_SHA: ${{ needs.validate-dispatch.outputs.runtime_sha }} - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - run: | - npm run acquire:runtime-packages -- \ - --version "$RUNTIME_VERSION" \ - --sha "$RUNTIME_SHA" \ - --output "$RUNNER_TEMP/runtime-packages" - - name: Archive validated runtime packages - run: tar -czf "$RUNNER_TEMP/runtime-packages.tar.gz" --exclude "runtime-packages/tarballs" -C "$RUNNER_TEMP" runtime-packages - - name: Upload validated runtime packages - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }} - path: ${{ runner.temp }}/runtime-packages.tar.gz - if-no-files-found: error - retention-days: 7 - - runtime-test: - name: Test runtime (${{ matrix.os }}, ${{ matrix.transport }}) - needs: [validate-dispatch, runtime-plan, runtime-acquire] - if: needs.validate-dispatch.outputs.test_policy != 'skipped' - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - transport: ["default", "inprocess"] - runs-on: ${{ matrix.os }} - environment: cicd - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - - name: Install test harness dependencies - working-directory: ./test/harness - run: npm ci --ignore-scripts - - name: Download validated runtime packages - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }} - path: ${{ runner.temp }}/runtime-package-artifact - - name: Extract validated runtime packages - run: | - runner_temp="$RUNNER_TEMP" - if command -v cygpath >/dev/null 2>&1; then - runner_temp="$(cygpath -u "$runner_temp")" - fi - rm -rf "$runner_temp/runtime-packages" - tar -xzf "$runner_temp/runtime-package-artifact/runtime-packages.tar.gz" -C "$runner_temp" - - name: Select the acquired runtime - env: - COPILOT_SDK_RUNTIME_PACKAGE_DIR: ${{ runner.temp }}/runtime-packages - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - run: | - node scripts/set-cli-version.js "$RUNTIME_VERSION" --local-package - runtime_path="$(npm run --silent prepare:runtime -- --print-path)" - echo "COPILOT_SDK_RUNTIME_PACKAGE_DIR=$COPILOT_SDK_RUNTIME_PACKAGE_DIR" >> "$GITHUB_ENV" - echo "COPILOT_CLI_PATH=$runtime_path" >> "$GITHUB_ENV" - - run: npm run build - - name: Warm up PowerShell - if: runner.os == 'Windows' - run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - name: Select inprocess transport - if: matrix.transport == 'inprocess' - run: echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV" - - name: Run Node SDK tests - id: e2e - continue-on-error: ${{ needs.validate-dispatch.outputs.test_policy == 'advisory' }} - env: - COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - run: npm test - - name: Report advisory E2E failure - if: needs.validate-dispatch.outputs.test_policy == 'advisory' && steps.e2e.outcome == 'failure' - env: - RUNNER_OS: ${{ runner.os }} - run: | - echo "::warning::Runtime-backed Node SDK E2E tests failed on ${RUNNER_OS}; continuing because test-policy is advisory." - { - echo "### Advisory runtime E2E failure" - echo - echo "Runtime-backed Node SDK E2E tests failed on **${RUNNER_OS}**. Publication remains eligible because \`test-policy\` is \`advisory\`." - } >> "$GITHUB_STEP_SUMMARY" - - runtime-package: - name: Build SDK packages - needs: [validate-dispatch, runtime-plan, runtime-acquire, runtime-test] - if: | - always() && - !cancelled() && - needs.validate-dispatch.result == 'success' && - needs.runtime-plan.result == 'success' && - needs.runtime-acquire.result == 'success' && - ( - needs.runtime-test.result == 'success' || - (needs.validate-dispatch.outputs.test_policy == 'skipped' && needs.runtime-test.result == 'skipped') - ) - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - cache: npm - cache-dependency-path: ./nodejs/package-lock.json - node-version: 22 - - run: npm ci --ignore-scripts - - name: Download validated runtime packages - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }} - path: ${{ runner.temp }}/runtime-package-artifact - - name: Extract validated runtime packages - run: | - runner_temp="$RUNNER_TEMP" - if command -v cygpath >/dev/null 2>&1; then - runner_temp="$(cygpath -u "$runner_temp")" - fi - rm -rf "$runner_temp/runtime-packages" - tar -xzf "$runner_temp/runtime-package-artifact/runtime-packages.tar.gz" -C "$runner_temp" - - name: Build and verify exact package set - env: - COPILOT_SDK_RUNTIME_PACKAGE_DIR: ${{ runner.temp }}/runtime-packages - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - SDK_VERSION: ${{ needs.runtime-plan.outputs.sdk_version }} - run: | - VERSION="$SDK_VERSION" node scripts/set-version.js - node scripts/set-cli-version.js "$RUNTIME_VERSION" --local-package - grep -F "COPILOT_CLI_USE_NPM_PACKAGE = false" src/cliVersion.ts - npm run build - npm run pack:release - npm run verify:release-packages - - name: Create immutable release manifest - env: - RELEASE_CHANNEL: ${{ inputs.dist-tag }} - RUNTIME_RUN_ID: ${{ needs.validate-dispatch.outputs.runtime_run_id }} - RUNTIME_SHA: ${{ needs.validate-dispatch.outputs.runtime_sha }} - RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }} - SDK_REF: ${{ github.ref }} - SDK_SHA: ${{ github.sha }} - SDK_VERSION: ${{ needs.runtime-plan.outputs.sdk_version }} - TEST_POLICY: ${{ needs.validate-dispatch.outputs.test_policy }} - WORKFLOW_CREATED_AT: ${{ needs.runtime-plan.outputs.workflow_created_at }} - WORKFLOW_RUN_ID: ${{ github.run_id }} - WORKFLOW_RUN_NUMBER: ${{ github.run_number }} - run: | - npm run release:manifest -- create release-manifest.json . - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ needs.runtime-plan.outputs.artifact_name }} - path: | - nodejs/release-manifest.json - nodejs/github-copilot-sdk-*.tgz - if-no-files-found: error - retention-days: 30 - - runtime-publish-internal: - name: Publish SDK internally - if: | - always() && - !cancelled() && - inputs.mode == 'publish' && - needs.runtime-plan.result == 'success' && - needs.runtime-package.result == 'success' - needs: [validate-dispatch, runtime-plan, runtime-package] - runs-on: ubuntu-latest - concurrency: - group: sdk-runtime-internal-${{ inputs.dist-tag }} - cancel-in-progress: false - queue: max - environment: cicd - permissions: - actions: read - contents: read - id-token: write - env: - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: 22 - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Download retained release - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: ${{ needs.runtime-plan.outputs.artifact_name }} - path: ./dist - - name: Validate retained release - run: | - node nodejs/node_modules/.bin/tsx nodejs/scripts/release-manifest.ts verify dist/release-manifest.json dist - [ "$(jq -r .workflow.runId dist/release-manifest.json)" = "${{ github.run_id }}" ] || - { echo "::error::Retained release belongs to a different workflow run."; exit 1; } - [ "$(jq -r .channel dist/release-manifest.json)" = "${{ inputs.dist-tag }}" ] || - { echo "::error::Retained release channel does not match the requested channel."; exit 1; } - - name: Azure login - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - allow-no-subscriptions: true - client-id: ${{ vars.CPD_ID_CLIENT_ID }} - tenant-id: ${{ vars.CPD_ID_TENANT_ID }} - - name: Configure authentication-only Azure npm access - run: | - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - printf '%s\n' \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > "$HOME/.npmrc" - - name: Publish exact tarballs internally - run: | - node nodejs/scripts/npm-release.js publish-manifest \ - dist/release-manifest.json dist "${{ inputs.dist-tag }}" "$FEED_URL" azure - - runtime-publish-public: - name: Publish SDK publicly - if: | - always() && - !cancelled() && - inputs.dist-tag == 'unstable' && - inputs.mode == 'publish' && - needs.runtime-plan.result == 'success' && - needs.runtime-publish-internal.result == 'success' - needs: [runtime-plan, runtime-publish-internal] - runs-on: ubuntu-latest - concurrency: - group: sdk-runtime-public-unstable - cancel-in-progress: false - queue: max - permissions: - actions: read - contents: read - id-token: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 - with: - node-version: 22 - - run: npm ci --ignore-scripts - working-directory: ./nodejs - - name: Update npm for trusted publishing - run: npm install -g npm@11.6.3 - - name: Download retained release - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: ${{ needs.runtime-plan.outputs.artifact_name }} - path: ./dist - - name: Validate retained release - run: | - node nodejs/node_modules/.bin/tsx nodejs/scripts/release-manifest.ts verify \ - dist/release-manifest.json dist - - name: Publish the same tarballs to public npm - run: | - node nodejs/scripts/npm-release.js publish-manifest \ - dist/release-manifest.json dist unstable https://registry.npmjs.org public diff --git a/.github/workflows/release-changelog.lock.yml b/.github/workflows/release-changelog.lock.yml index c66be2f35b..35e1ada2a3 100644 --- a/.github/workflows/release-changelog.lock.yml +++ b/.github/workflows/release-changelog.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6abb56e7d33f12df48df547a0f4143936049b45de28d4f1ff92bdc8020aa57c0","body_hash":"264021ebf1d0bc74660b753fafc82d43a8ef6c34311da38d757b91b959f8481b","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6abb56e7d33f12df48df547a0f4143936049b45de28d4f1ff92bdc8020aa57c0","body_hash":"74115979906c43791724a7a021f11f23493d9eb4e4867530ecd03555a0ef29d9","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["missing_data","missing_tool","noop","update_release"]}]} # This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # diff --git a/.github/workflows/release-changelog.md b/.github/workflows/release-changelog.md index 6729eef47c..eb445e5dbc 100644 --- a/.github/workflows/release-changelog.md +++ b/.github/workflows/release-changelog.md @@ -37,6 +37,8 @@ Determine which type of release this is by inspecting the tag or fetching the re Use the GitHub API to fetch the release corresponding to `${{ github.event.inputs.tag }}` to get its name, publish date, prerelease status, and other metadata. +Use only this public SDK repository's source, history, releases, and pull requests. Do not retrieve or link private runtime source/history. If the target is not a published SDK `v` release, stop without updating anything; `runtime-*` releases contain acquisition assets, not SDK release notes. + ## Your Task ### Step 1: Identify the version range @@ -49,6 +51,7 @@ Use the GitHub API to fetch the release corresponding to `${{ github.event.input 2. The **new version** is the release tag: `${{ github.event.inputs.tag }}` 3. Fetch the release metadata to determine if this is a **stable** or **prerelease** release. 4. Determine the **previous version** to diff against: + - Consider only published, non-draft SDK `v` releases whose publication time precedes the target. Exclude `runtime-*` releases, even for prerelease comparisons. - **For stable releases**: list releases via the API and find the previous **stable** release (skip prereleases and the current release). Do not use the frozen `CHANGELOG.md` as the version baseline. Stable release notes include ALL changes since the last stable release, even if some were already mentioned in prerelease notes. - **For prerelease releases**: find the most recent release of **any kind** (stable or prerelease) that precedes this one. This way prerelease notes only cover what's new since the last release. 5. If no previous release exists at all, use the first commit in the repo as the starting point. diff --git a/.github/workflows/sdk-dotnet.yml b/.github/workflows/sdk-dotnet.yml index 4514817827..63b689f94e 100644 --- a/.github/workflows/sdk-dotnet.yml +++ b/.github/workflows/sdk-dotnet.yml @@ -1,6 +1,6 @@ # Invoked by sdk.yml to run the .NET SDK build/test matrix, documentation, -# backend coverage, full subprocess CAPI coverage, and focused in-process smoke -# coverage on Linux and x64 musl. +# full subprocess CAPI coverage, and focused in-process smoke +# coverage across standard platforms and x64 musl. name: "SDK .NET" env: @@ -25,13 +25,9 @@ permissions: jobs: dotnet-linux-x64: - name: ".NET (ubuntu-latest, ${{ matrix.transport }}, CAPI)" + name: ".NET (ubuntu-latest, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'linux-x64' timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] runs-on: ubuntu-latest defaults: run: @@ -39,16 +35,15 @@ jobs: working-directory: ${{ inputs.sdk-home }}/dotnet env: COPILOT_SDK_E2E_BACKEND: capi - DOTNET_TEST_FILTER: ${{ matrix.transport == 'inprocess' && 'FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication' || '' }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-gnu + artifact-name: executable-sdk-Linux-X64-gnu path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -61,11 +56,10 @@ jobs: - run: dotnet restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' run: dotnet format --verify-no-changes --no-restore - run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - name: Test structured output with reflection enabled - if: matrix.transport == 'default' env: DOTNET_ROLL_FORWARD: Major run: >- @@ -76,21 +70,38 @@ jobs: --filter "FullyQualifiedName~GitHub.Copilot.Test.Unit.ClientSessionLifetimeTests.StructuredOutput" - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: ../scripts/ci/run-dotnet-tests.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" + run: ../scripts/ci/run-dotnet-tests.sh + - name: Validate documentation examples + if: github.event_name != 'merge_group' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:cs - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-ubuntu-latest-${{ matrix.transport }}-capi-${{ github.run_attempt }} + name: dotnet-test-diagnostics-ubuntu-latest-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 dotnet-darwin-arm64: - name: ".NET (macos-latest, default, CAPI)" + name: ".NET (macos-26-xlarge, default, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'darwin-arm64' - runs-on: macos-latest + runs-on: macos-26-xlarge timeout-minutes: 30 defaults: run: @@ -98,14 +109,15 @@ jobs: working-directory: ${{ inputs.sdk-home }}/dotnet env: COPILOT_SDK_E2E_BACKEND: capi + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-macOS-ARM64 + artifact-name: executable-sdk-macOS-ARM64 path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -119,13 +131,23 @@ jobs: - run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test out-of-process + id: subprocess + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: ../scripts/ci/run-dotnet-tests.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" run: ../scripts/ci/run-dotnet-tests.sh - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-macos-latest-default-capi-${{ github.run_attempt }} + name: dotnet-test-diagnostics-macos-26-xlarge-default-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 @@ -142,14 +164,15 @@ jobs: env: COPILOT_SDK_E2E_BACKEND: capi DOTNET_TEST_RUNTIME: win-x64 + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/subprocess steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Windows-X64 + artifact-name: executable-sdk-Windows-X64 path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -164,86 +187,42 @@ jobs: - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: ../scripts/ci/run-dotnet-tests.sh - - if: failure() - uses: actions/upload-artifact@v7 - with: - name: dotnet-test-diagnostics-windows-latest-default-capi-${{ github.run_attempt }} - path: ${{ inputs.sdk-home }}/dotnet/TestResults/ - if-no-files-found: warn - retention-days: 7 - - # Exercise non-CAPI model backends through .NET only instead of multiplying - # them across every language and execution mode; all SDKs cover both modes with CAPI. - dotnet-backends: - name: ".NET (ubuntu-latest, default, ${{ matrix.backend }})" - if: inputs.platform == 'all' || inputs.platform == 'linux-x64' - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - backend: [anthropic-messages, openai-responses, openai-completions] - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/dotnet - env: - COPILOT_SDK_E2E_BACKEND: ${{ matrix.backend }} - DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/download-artifact@v8 - with: - name: executable-sdk-Linux-X64-gnu - path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare - working-directory: . - - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - uses: actions/setup-node@v6 - with: - node-version: 22 - - run: dotnet restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - - run: dotnet build --no-restore -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" -p:CopilotSkipCliDownload=true - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + DOTNET_TEST_RESULTS_DIRECTORY: TestResults/inprocess + DOTNET_TEST_FILTER: "FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication" run: ../scripts/ci/run-dotnet-tests.sh - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-ubuntu-latest-default-${{ matrix.backend }}-${{ github.run_attempt }} + name: dotnet-test-diagnostics-windows-latest-default-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 dotnet-musl-x64: - name: ".NET (Alpine x64, ${{ matrix.transport }}, CAPI)" + name: ".NET (Alpine x64, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 20 env: COPILOT_SDK_E2E_BACKEND: capi - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -255,7 +234,6 @@ jobs: image: mcr.microsoft.com/dotnet/sdk:10.0-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }} - transport: ${{ matrix.transport }} command: | dotnet --info | grep -Eq "RID:[[:space:]]+linux-musl-x64" apk add --no-cache nodejs npm @@ -278,50 +256,33 @@ jobs: dotnet restore "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" \ -p:CopilotSkipCliDownload=true - set -- - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - set -- --filter 'FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication' - fi - dotnet test "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ - --no-restore \ + dotnet build "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ + --no-restore --framework net8.0 \ + -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" \ + -p:CopilotSkipCliDownload=true \ + -p:RunAnalyzers=false + set -- "$COPILOT_SDK_ROOT/dotnet/test/GitHub.Copilot.SDK.Test.csproj" \ + --no-build \ --framework net8.0 \ -v n \ --blame-hang \ --blame-hang-timeout 10m \ --blame-hang-dump-type none \ --logger "trx;LogFilePrefix=test-results" \ - --results-directory "$COPILOT_SDK_ROOT/dotnet/TestResults" \ - "$@" \ -p:CopilotCliBinaryPath="$COPILOT_RUNTIME_BINARY_PATH" \ -p:CopilotSkipCliDownload=true \ -p:RunAnalyzers=false + status=0 + dotnet test "$@" --results-directory "$COPILOT_SDK_ROOT/dotnet/TestResults/subprocess" || status=$? + COPILOT_SDK_DEFAULT_CONNECTION=inprocess dotnet test "$@" \ + --results-directory "$COPILOT_SDK_ROOT/dotnet/TestResults/inprocess" \ + --filter 'FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientE2ETests.Should_Start_And_Connect_Over_InProcess_Ffi|FullyQualifiedName~GitHub.Copilot.Test.E2E.SessionE2ETests.Should_Receive_Session_Events|FullyQualifiedName~GitHub.Copilot.Test.E2E.ClientOptionsE2ETests.Should_Use_Configured_GitHub_Host_For_Authentication' || status=$? + exit "$status" - name: Upload .NET test diagnostics if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-alpine-x64-${{ matrix.transport }}-capi-${{ github.run_attempt }} + name: dotnet-test-diagnostics-alpine-x64-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 - - docs-dotnet: - name: "Docs (.NET)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: dotnet restore -p:CopilotSkipCliDownload=true - working-directory: ${{ inputs.sdk-home }}/dotnet - - run: npm run extract && npm run validate:cs - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-go.yml b/.github/workflows/sdk-go.yml index f613027d39..e7235998f8 100644 --- a/.github/workflows/sdk-go.yml +++ b/.github/workflows/sdk-go.yml @@ -25,13 +25,12 @@ permissions: jobs: go: - name: "Go (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Go (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -42,9 +41,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -56,52 +55,62 @@ jobs: ${{ inputs.sdk-home }}/go/samples/go.sum # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: | go fmt ./... git diff --exit-code - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' uses: golangci/golangci-lint-action@v9 with: working-directory: ${{ inputs.sdk-home }}/go version: latest args: --timeout=5m - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: go test ./... working-directory: ${{ inputs.sdk-home }}/go/samples - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: /bin/bash test.sh + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} - GOFLAGS: ${{ matrix.transport == 'inprocess' && '-tags=copilot_inprocess' || '' }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + GOFLAGS: -tags=copilot_inprocess + run: | + go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' + go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' + uses: actions/setup-node@v6 + with: + node-version: 22 + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation run: | - if [ "$COPILOT_SDK_DEFAULT_CONNECTION" = "inprocess" ]; then - go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' - go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' - else - /bin/bash test.sh - fi + npm ci + npm run extract + npm run validate:go go-musl-x64: - name: "Go (Alpine x64, ${{ matrix.transport }})" + name: "Go (Alpine x64)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -112,7 +121,6 @@ jobs: image: golang:1.24-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }} - transport: ${{ matrix.transport }} command: | apk add --no-cache build-base nodejs npm npm --prefix "$COPILOT_SDK_ROOT/nodejs" ci --ignore-scripts @@ -130,39 +138,14 @@ jobs: *) echo "Expected the linuxmusl-x64 runtime, got: $COPILOT_CLI_PATH" >&2; exit 1 ;; esac - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - export GOFLAGS="-tags=copilot_inprocess" - else - unset GOFLAGS - fi export CGO_ENABLED=1 test "$(go env GOARCH)" = "amd64" test "$(go env CGO_ENABLED)" = "1" cd "$COPILOT_SDK_ROOT/go" - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' - go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' - else - /bin/bash test.sh - fi - - docs-go: - name: "Docs (Go)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-go@v6 - with: - go-version: "1.24" - cache-dependency-path: ${{ inputs.sdk-home }}/go/go.sum - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:go - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + status=0 + /bin/bash test.sh || status=$? + export COPILOT_SDK_DEFAULT_CONNECTION=inprocess + export GOFLAGS="-tags=copilot_inprocess" + go test -v -race -timeout=20m ./internal/e2e -run '^TestInProcessFfiE2E$' || status=$? + go test -v -race -timeout=20m ./internal/e2e -run '^TestClientE2E$/^should_use_configured_github_host_for_authentication$' || status=$? + exit "$status" diff --git a/.github/workflows/sdk-java.yml b/.github/workflows/sdk-java.yml index a69caa71d3..c4807d22cd 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -1,5 +1,5 @@ -# Invoked by sdk.yml to run full subprocess Java SDK coverage on supported JDKs, -# focused in-process smoke coverage across native classifiers, and documentation. +# Invoked by sdk.yml to run full subprocess Java SDK coverage followed by focused +# in-process smoke on standard platforms and x64 musl, plus JDK 17 and docs checks. name: "SDK Java" env: @@ -24,13 +24,19 @@ permissions: jobs: java: - name: "Java (JDK ${{ matrix.test-jdk }})" - if: inputs.platform == 'all' || inputs.platform == 'linux-x64' + name: "Java (${{ matrix.os }}, JDK ${{ matrix.test-jdk }})" + if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} test-jdk: ["25", "17"] - runs-on: ubuntu-latest + exclude: + - os: macos-26 + test-jdk: "17" + - os: windows-latest + test-jdk: "17" + runs-on: ${{ matrix.os }} defaults: run: shell: bash @@ -40,9 +46,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-gnu + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -56,14 +62,17 @@ jobs: node-version: 22 # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./scripts/test-update-documentation-versions.sh - run: ./mvnw test-compile jar:jar - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./mvnw javadoc:javadoc - - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' run: ./mvnw spotless:check + - if: runner.os == 'Windows' + run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - if: matrix.test-jdk == '25' + id: subprocess env: CI: "true" run: ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" @@ -76,84 +85,121 @@ jobs: env: CI: "true" run: ./mvnw -pl sdk 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 -Dcopilot.cli.path="$COPILOT_CLI_PATH" + - name: Test in-process smoke + if: ${{ !cancelled() && matrix.test-jdk == '25' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + CI: "true" + run: | + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ + -Dtest=NoTestsForInProcessSmoke \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dit.test=InProcessTransportIT,AuthHostE2ETest \ + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" + - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' + run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.test-jdk == '25' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:java - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-jdk-${{ matrix.test-jdk }} + name: java-test-results-${{ matrix.os }}-jdk-${{ matrix.test-jdk }} path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - java-inprocess: - name: "Java (inprocess, ${{ matrix.classifier }})" - if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.platform == 'linux-x64' && '[{"os":"ubuntu-latest","classifier":"linux-x64"}]' || inputs.platform == 'darwin-arm64' && '[{"os":"macos-26","classifier":"darwin-arm64"}]' || inputs.platform == 'win32-x64' && '[{"os":"windows-latest","classifier":"win32-x64"}]' || '[{"os":"ubuntu-latest","classifier":"linux-x64"},{"os":"windows-latest","classifier":"win32-x64"},{"os":"macos-26","classifier":"darwin-arm64"}]') }} - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/java + java-musl-x64: + name: "Java (JDK 25, linuxmusl-x64)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' + runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . - - uses: actions/setup-java@v5 - with: - java-version: "25" - distribution: microsoft - cache: maven - - uses: actions/setup-node@v6 + - uses: ./.github/actions/run-alpine-tests + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} with: - node-version: 22 - - env: - CI: "true" - MAVEN_ARGS: ${{ matrix.maven-args }} - run: >- - ./mvnw clean verify -Pinprocess - -Dtest=NoTestsForInProcessSmoke - -Dsurefire.failIfNoSpecifiedTests=false - -Dit.test=InProcessTransportIT,AuthHostE2ETest - -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" - $MAVEN_ARGS + image: eclipse-temurin:25-jdk-alpine + sdk-root: /workspace/${{ inputs.sdk-home }} + workdir: /workspace/${{ inputs.sdk-home }}/java + command: | + apk add --no-cache java-cacerts maven nodejs npm + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts + export GITHUB_ENV=/tmp/copilot-sdk-runtime.env + export COPILOT_RUNTIME_TARGET=linuxmusl-x64 + export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64 + node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare + set -a + . "$GITHUB_ENV" + set +a + node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 + ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl + status=0 + ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$? + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ + -Dtest=NoTestsForInProcessSmoke \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dit.test=InProcessTransportIT,AuthHostE2ETest \ + -Dcopilot.native.libc=musl \ + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$? + exit "$status" - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-inprocess-${{ matrix.classifier }} + name: java-test-results-linuxmusl-x64 path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - - java-inprocess-musl-x64: - name: "Java (inprocess, linuxmusl-x64)" - if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' - runs-on: ubuntu-latest + java-musl-arm64: + name: "Java (JDK 25, linuxmusl-arm64)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-arm64' + runs-on: ubuntu-24.04-arm timeout-minutes: 30 steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-ARM64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . - - name: Run Java SDK tests (InProcess, musl) + - name: Run Java SDK tests (linuxmusl-arm64) env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: | @@ -164,56 +210,44 @@ jobs: --env COPILOT_HMAC_KEY \ --env GITHUB_ACTIONS=true \ --env GITHUB_WORKSPACE=/workspace \ - --env SDK_HOME="/workspace/$SDK_HOME" \ + --env COPILOT_SDK_ROOT="/workspace/$SDK_HOME" \ eclipse-temurin:25-jdk-alpine \ sh -c 'set -eux apk add --no-cache git java-cacerts maven nodejs npm - export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts git config --global --add safe.directory /workspace + export JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts export GITHUB_ENV=/tmp/copilot-sdk-runtime.env - export COPILOT_RUNTIME_TARGET=linuxmusl-x64 - export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-x64 - node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + export COPILOT_RUNTIME_TARGET=linuxmusl-arm64 + export COPILOT_RUNTIME_OUTPUT_DIRECTORY=/workspace/.sdk-runtime/linuxmusl-arm64 + node "$COPILOT_SDK_ROOT/scripts/ci/runtime-artifact.mjs" prepare set -a . "$GITHUB_ENV" set +a - node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 - ./mvnw clean verify -Pinprocess \ + node copilot-native/scripts/validate-native-host.mjs linuxmusl-arm64 + ./mvnw test-compile jar:jar -Dcopilot.native.libc=musl + status=0 + ./mvnw -pl sdk verify -Dskip.test.harness=true -Dcopilot.cli.path="$COPILOT_CLI_PATH" || status=$? + mkdir -p sdk/target/subprocess-reports + for reports in surefire-reports surefire-reports-isolated failsafe-reports; do + if [ -d "sdk/target/$reports" ]; then + mv "sdk/target/$reports" sdk/target/subprocess-reports/ + fi + done + ./mvnw verify -Pinprocess \ + -Dskip.test.harness=true \ -Dtest=NoTestsForInProcessSmoke \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dit.test=InProcessTransportIT,AuthHostE2ETest \ -Dcopilot.native.libc=musl \ - -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH"' + -Dcopilot.inprocess.cli.path="$COPILOT_CLI_PATH" || status=$? + exit "$status"' - if: failure() uses: actions/upload-artifact@v7 with: - name: java-test-results-inprocess-linuxmusl-x64 + name: java-test-results-linuxmusl-arm64 path: | ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports/ ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-reports/ + ${{ inputs.sdk-home }}/java/sdk/target/subprocess-reports/ retention-days: 7 - - docs-java: - name: "Docs (Java)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-java@v5 - with: - distribution: microsoft - java-version: "25" - cache: maven - - run: ./mvnw install -Dmaven.test.skip=true -Dskip.test.harness=true -Dcopilot.native.skip.download=true - working-directory: ${{ inputs.sdk-home }}/java - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:java - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-nodejs.yml b/.github/workflows/sdk-nodejs.yml index 8ea69933cd..55037d3f38 100644 --- a/.github/workflows/sdk-nodejs.yml +++ b/.github/workflows/sdk-nodejs.yml @@ -19,19 +19,23 @@ on: sdk-home: required: true type: string + outputs: + capi-result: + description: "Standard-platform CAPI job result, independent of BYOK jobs" + # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495 + value: ${{ fromJSON(toJSON(jobs.nodejs)).result }} permissions: contents: read jobs: nodejs: - name: "Node.js (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Node.js (${{ matrix.os }}, CAPI)" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -42,9 +46,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -54,16 +58,19 @@ jobs: cache: npm cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json - run: npm ci --ignore-scripts + - name: Install generator test dependencies + run: npm ci --ignore-scripts + working-directory: ${{ inputs.sdk-home }}/scripts/codegen # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run format:check - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run lint - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: npm run typecheck - run: npm run build - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' env: COPILOT_SDK_RUNTIME_PLATFORMS: linux-x64 run: | @@ -71,43 +78,104 @@ jobs: npm run verify:release-packages - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm test working-directory: ${{ inputs.sdk-home }}/test/harness - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm ci working-directory: ${{ inputs.sdk-home }}/scripts/corrections - - if: runner.os == 'Linux' && matrix.transport == 'default' + - if: runner.os == 'Linux' run: npm test working-directory: ${{ inputs.sdk-home }}/scripts/corrections - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} run: | - if [ "$COPILOT_SDK_DEFAULT_CONNECTION" = "inprocess" ]; then - npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts + if [ "$GITHUB_EVENT_NAME" = "merge_group" ]; then + npm test -- --reporter=default --reporter=json --outputFile="$RUNNER_TEMP/sdk-nodejs-results.json" else npm test fi + - name: Upload merge-queue Node.js test results + if: always() && github.event_name == 'merge_group' && runner.os == 'Linux' + continue-on-error: true + uses: actions/upload-artifact@v7 + with: + name: suspected-flakes-sdk-nodejs-${{ github.run_attempt }}-linux + path: ${{ runner.temp }}/sdk-nodejs-results.json + if-no-files-found: warn + retention-days: 7 + - name: Test in-process smoke + if: ${{ !cancelled() && github.event_name != 'merge_group' && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + run: npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + run: | + npm ci + npm run extract + npm run validate:ts - nodejs-musl-x64: - name: "Node.js (Alpine x64, ${{ matrix.transport }})" - if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' + # Keep each BYOK backend on its own runner; shared runtime E2E coverage belongs + # in TypeScript rather than repeating these sweeps across SDK languages. + nodejs-backends: + name: "Node.js (ubuntu-latest, ${{ matrix.backend }})" + if: github.event_name != 'merge_group' && (inputs.platform == 'all' || inputs.platform == 'linux-x64') runs-on: ubuntu-latest + timeout-minutes: 30 strategy: fail-fast: false matrix: - transport: [default, inprocess] + backend: [anthropic-messages, openai-responses, openai-completions] + defaults: + run: + shell: bash + working-directory: ${{ inputs.sdk-home }}/nodejs + env: + COPILOT_SDK_E2E_BACKEND: ${{ matrix.backend }} + steps: + - uses: actions/checkout@v7 + timeout-minutes: 4 + with: + persist-credentials: false + - uses: ./.github/actions/download-sdk-runtime + with: + artifact-name: executable-sdk-Linux-X64-gnu + path: ${{ github.workspace }} + - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare + working-directory: . + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json + - run: npm ci --ignore-scripts + - run: npm run build + - run: npm ci --ignore-scripts + working-directory: ${{ inputs.sdk-home }}/test/harness + - name: Test out-of-process E2Es + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: npm test -- test/e2e + + nodejs-musl-x64: + name: "Node.js (Alpine x64, CAPI)" + if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' + runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -118,9 +186,9 @@ jobs: image: node:22-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }}/nodejs - transport: ${{ matrix.transport }} command: | npm ci --ignore-scripts + (cd "$COPILOT_SDK_ROOT/scripts/codegen" && npm ci --ignore-scripts) npm run build (cd "$COPILOT_SDK_ROOT/test/harness" && npm ci --ignore-scripts) @@ -137,29 +205,7 @@ jobs: esac test -x "$COPILOT_CLI_PATH" test -f "$(dirname "$COPILOT_CLI_PATH")/runtime.node" - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts - else - npm test - fi - - docs-typescript: - name: "Docs (TypeScript)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: npm - cache-dependency-path: ${{ inputs.sdk-home }}/nodejs/package-lock.json - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/nodejs - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract && npm run validate:ts - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation + status=0 + npm test || status=$? + COPILOT_SDK_DEFAULT_CONNECTION=inprocess npm test -- test/e2e/inprocess_ffi.e2e.test.ts test/e2e/auth_host.e2e.test.ts || status=$? + exit "$status" diff --git a/.github/workflows/sdk-platform.yml b/.github/workflows/sdk-platform.yml index 2fa62c3b1e..f1ed4e6ed4 100644 --- a/.github/workflows/sdk-platform.yml +++ b/.github/workflows/sdk-platform.yml @@ -1,5 +1,5 @@ -# Runs all SDK language checks for one runtime platform, allowing each platform -# to start as soon as its runtime artifact is available. +# Runs SDK language checks for one runtime platform as soon as its artifact is +# available. Merge groups retain only the Node.js compatibility lane. name: "SDK platform" on: @@ -11,6 +11,14 @@ on: sdk-home: required: true type: string + outputs: + nodejs-result: + description: "Node.js SDK result, independent of other languages" + # Direct result outputs can be empty: https://github.com/actions/runner/issues/2495 + value: ${{ fromJSON(toJSON(jobs.nodejs)).result }} + nodejs-capi-result: + description: "Standard-platform Node.js CAPI result, independent of BYOK and other languages" + value: ${{ jobs.nodejs.outputs.capi-result }} permissions: contents: read @@ -24,6 +32,7 @@ jobs: secrets: inherit python: + if: github.event_name != 'merge_group' uses: ./.github/workflows/sdk-python.yml with: platform: ${{ inputs.platform }} @@ -31,6 +40,7 @@ jobs: secrets: inherit go: + if: github.event_name != 'merge_group' uses: ./.github/workflows/sdk-go.yml with: platform: ${{ inputs.platform }} @@ -38,6 +48,7 @@ jobs: secrets: inherit dotnet: + if: github.event_name != 'merge_group' uses: ./.github/workflows/sdk-dotnet.yml with: platform: ${{ inputs.platform }} @@ -45,6 +56,7 @@ jobs: secrets: inherit rust: + if: github.event_name != 'merge_group' uses: ./.github/workflows/sdk-rust.yml with: platform: ${{ inputs.platform }} @@ -52,6 +64,7 @@ jobs: secrets: inherit java: + if: github.event_name != 'merge_group' uses: ./.github/workflows/sdk-java.yml with: platform: ${{ inputs.platform }} diff --git a/.github/workflows/sdk-python.yml b/.github/workflows/sdk-python.yml index fa56ff533e..2abeca3d8a 100644 --- a/.github/workflows/sdk-python.yml +++ b/.github/workflows/sdk-python.yml @@ -25,13 +25,12 @@ permissions: jobs: python: - name: "Python (${{ matrix.os }}, ${{ matrix.transport }})" + name: "Python (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} - transport: [default, inprocess] + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26","windows-latest"]') }} runs-on: ${{ matrix.os }} timeout-minutes: 20 defaults: @@ -45,9 +44,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -63,52 +62,58 @@ jobs: - run: uv sync --locked --all-extras --dev # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain # static checks; merge groups rerun only SDK compatibility coverage. - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: uv run --locked ruff format --check . - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: uv run --locked ruff check - - if: github.event_name != 'merge_group' && runner.os == 'Linux' && matrix.transport == 'default' + - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: uv run --locked ty check copilot - run: npm ci --ignore-scripts working-directory: ${{ inputs.sdk-home }}/test/harness - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile + - name: Test in-process smoke + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - COPILOT_SDK_DEFAULT_CONNECTION: ${{ matrix.transport == 'inprocess' && 'inprocess' || '' }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess + run: uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py + - name: Validate documentation examples + if: github.event_name != 'merge_group' && runner.os == 'Linux' + working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation run: | - if [ "$COPILOT_SDK_DEFAULT_CONNECTION" = "inprocess" ]; then - uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py - else - env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile - fi + npm ci + npm run extract + uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version + uv run --locked --python 3.12 --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py - name: Upload Python test diagnostics if: failure() uses: actions/upload-artifact@v7 with: - name: python-test-diagnostics-${{ matrix.os }}-${{ matrix.transport }}-${{ github.run_attempt }} + name: python-test-diagnostics-${{ matrix.os }}-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/python/.pytest-diagnostics/ include-hidden-files: true if-no-files-found: warn retention-days: 7 python-musl-x64: - name: "Python (Alpine x64, ${{ matrix.transport }})" + name: "Python (Alpine x64)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -119,10 +124,9 @@ jobs: image: python:3.11-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }} - transport: ${{ matrix.transport }} command: | apk add --no-cache nodejs npm - python -m pip install --no-cache-dir uv + python -m pip install --no-cache-dir --timeout 120 uv npm --prefix "$COPILOT_SDK_ROOT/nodejs" ci --ignore-scripts npm --prefix "$COPILOT_SDK_ROOT/test/harness" ci --ignore-scripts @@ -140,43 +144,16 @@ jobs: cd "$COPILOT_SDK_ROOT/python" uv sync --locked --all-extras --dev - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py - else - env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile - fi + status=0 + env -u COPILOT_SKIP_CLI_DOWNLOAD uv run --locked pytest -v -s -n 2 --dist=loadfile || status=$? + COPILOT_SDK_DEFAULT_CONNECTION=inprocess uv run --locked pytest -v -s e2e/test_inprocess_ffi_e2e.py e2e/test_auth_host_e2e.py || status=$? + exit "$status" - name: Upload Python test diagnostics if: failure() uses: actions/upload-artifact@v7 with: - name: python-test-diagnostics-alpine-x64-${{ matrix.transport }}-${{ github.run_attempt }} + name: python-test-diagnostics-alpine-x64-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/python/.pytest-diagnostics/ include-hidden-files: true if-no-files-found: warn retention-days: 7 - - docs-python: - name: "Docs (Python)" - if: inputs.platform == 'all' || inputs.platform == 'docs' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 22 - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - uses: astral-sh/setup-uv@v7 - - run: uv sync --locked - working-directory: ${{ inputs.sdk-home }}/python - - run: npm ci - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: npm run extract - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation - - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" python3 -m mypy --version - - run: uv run --locked --project "$GITHUB_WORKSPACE/$SDK_HOME/python" npm run validate:py - working-directory: ${{ inputs.sdk-home }}/scripts/docs-validation diff --git a/.github/workflows/sdk-runtime-artifact.yml b/.github/workflows/sdk-runtime-artifact.yml index b2c8532f45..0481e945ae 100644 --- a/.github/workflows/sdk-runtime-artifact.yml +++ b/.github/workflows/sdk-runtime-artifact.yml @@ -3,6 +3,9 @@ name: "SDK runtime artifact" env: COPILOT_AUTO_UPDATE: "false" COPILOT_BAZEL_ANG_ENABLED: ${{ vars.COPILOT_BAZEL_ANG_ENABLED }} + COPILOT_BAZEL_ANG_SCOPE: ${{ vars.COPILOT_BAZEL_ANG_SCOPE }} + COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE }} + COPILOT_BAZEL_ANG_PR_SAMPLED_MODE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLED_MODE }} COPILOT_RUNTIME_E2E_TEST_HOOKS: "1" CARGO_PROFILE_DEV_DEBUG: line-tables-only GIT_HTTP_LOW_SPEED_LIMIT: 1000 @@ -52,13 +55,96 @@ on: permissions: contents: read +concurrency: + group: ${{ inputs.shared-cli-build-compatible && format('project-build-sea-{0}-release-{1}-{2}', inputs.os, inputs.bazel-config, github.sha) || format('sdk-runtime-artifact-{0}-{1}', inputs.target, github.sha) }} + queue: max + jobs: + cache-relay: + if: inputs.runtime-source == 'checkout' + runs-on: ubuntu-slim + timeout-minutes: 15 + permissions: + actions: read + contents: read + outputs: + cache-hit: ${{ steps.cache.outputs.cache-hit }} + steps: + - uses: actions/checkout@v7 + timeout-minutes: 4 + with: + persist-credentials: false + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Capture verified addon source identity + run: node --experimental-strip-types script/resolve-cli-build-identity.ts --github-env + + - name: Restore completed runtime build + id: cache + uses: actions/cache/restore@v6 + with: + path: | + project-build-receipt.txt + dist-cli/ + dist-tmp/release-downloads/ripgrep/ + dist-tmp/release-downloads/tgrep/ + dist-bin/ + dist-pkg-tarballs/ + src/core/sharedApi/runtime-generated/index.d.ts + src/native/runtime/runtime.*.node.metadata + src/native/runtime/copilot-runtime.*.profile + src/native/runtime/index.d.ts + src/native/cli/index.d.ts + !dist-cli/sea.blob + !dist-cli/github-copilot-*.tgz + key: ${{ inputs.shared-cli-build-compatible && format('project-build-v4-sea-{0}-{1}-release-{2}-{3}-{4}', inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) || format('sdk-project-build-v4-{0}-{1}-{2}-release-{3}-{4}-{5}', inputs.target, inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) }} + enableCrossOsArchive: "true" + + - name: Restore cached source-layout addons and executable bits + if: steps.cache.outputs.cache-hit == 'true' + run: node script/restore-cli-build.mjs + + - name: Verify completed runtime build + if: steps.cache.outputs.cache-hit == 'true' + shell: bash + env: + TARGET: ${{ inputs.target }} + run: | + extension= + if [[ "$TARGET" == win32-* ]]; then + extension=.exe + fi + test -s project-build-receipt.txt + test -s "dist-cli/prebuilds/$TARGET/runtime.node" + test -s "dist-cli/prebuilds/$TARGET/cli-native.node" + test -s "dist-cli/prebuilds/$TARGET/copilot-runtime$extension" + test -s "dist-bin/$TARGET/copilot$extension" + + - name: Upload SDK runtime artifact + if: steps.cache.outputs.cache-hit == 'true' + uses: actions/upload-artifact@v7 + with: + name: executable-sdk-${{ inputs.artifact }} + path: | + dist-cli/ + dist-bin/${{ inputs.target }}/ + !dist-cli/sea.blob + !dist-cli/github-copilot-*.tgz + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + build: + needs: cache-relay + if: >- + !cancelled() && + (inputs.runtime-source == 'published' || + (needs.cache-relay.result == 'success' && needs.cache-relay.outputs.cache-hit != 'true')) runs-on: ${{ inputs.runtime-source == 'checkout' && inputs.os || 'ubuntu-latest' }} timeout-minutes: ${{ inputs.timeout-minutes }} - concurrency: - group: ${{ inputs.shared-cli-build-compatible && format('project-build-sea-{0}-release-{1}-{2}', inputs.os, inputs.bazel-config, github.sha) || format('sdk-runtime-artifact-{0}-{1}', inputs.target, github.sha) }} - queue: max permissions: actions: read contents: read @@ -70,6 +156,17 @@ jobs: with: persist-credentials: false + # Capture identity before caches, dependency setup, or downloaded addons + # can make the checkout appear dirty to metadata verification. + - uses: actions/setup-node@v6 + if: inputs.runtime-source == 'checkout' + with: + node-version-file: .nvmrc + + - name: Capture verified addon source identity + if: inputs.runtime-source == 'checkout' + run: node --experimental-strip-types script/resolve-cli-build-identity.ts --github-env + # Restore/save paths must match shared-cli-build.yml in the same order: # Actions includes the path list in the cache version, independently of the key. - name: Restore completed runtime build @@ -91,7 +188,8 @@ jobs: src/native/cli/index.d.ts !dist-cli/sea.blob !dist-cli/github-copilot-*.tgz - key: ${{ inputs.shared-cli-build-compatible && format('project-build-v3-sea-{0}-{1}-{2}-release-{3}-{4}-{5}', inputs.os, runner.os, runner.arch, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) || format('sdk-project-build-v3-{0}-{1}-{2}-{3}-release-{4}-{5}-{6}', inputs.target, inputs.os, runner.os, runner.arch, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) }} + key: ${{ inputs.shared-cli-build-compatible && format('project-build-v4-sea-{0}-{1}-release-{2}-{3}-{4}', inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) || format('sdk-project-build-v4-{0}-{1}-{2}-release-{3}-{4}-{5}', inputs.target, inputs.os, inputs.target, inputs.bazel-config, inputs.use-bazel-addons-on-darwin, github.sha) }} + enableCrossOsArchive: "true" - name: Restore cached source-layout addons and executable bits if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit == 'true' @@ -101,6 +199,10 @@ jobs: uses: ./.github/actions/setup-node timeout-minutes: ${{ fromJSON(env.SETUP_NODE_TIMEOUT_MINUTES) }} + - name: Capture verified addon source identity + if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit != 'true' + run: node --experimental-strip-types script/resolve-cli-build-identity.ts --github-env + - if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit != 'true' && inputs.prebuilt-addons-artifact == '' && !(runner.os == 'Windows' && inputs.target == 'win32-arm64') uses: ./.github/actions/setup-bazel with: @@ -179,6 +281,7 @@ jobs: !dist-cli/sea.blob !dist-cli/github-copilot-*.tgz key: ${{ steps.cache.outputs.cache-primary-key }} + enableCrossOsArchive: "true" - name: Upload SDK runtime artifact uses: actions/upload-artifact@v7 diff --git a/.github/workflows/sdk-rust.yml b/.github/workflows/sdk-rust.yml index ff875cda73..abe13e28bb 100644 --- a/.github/workflows/sdk-rust.yml +++ b/.github/workflows/sdk-rust.yml @@ -25,13 +25,13 @@ permissions: jobs: rust: - name: "Rust (${{ matrix.os }}, default)" + name: "Rust (${{ matrix.os }})" if: contains(fromJSON('["all","linux-x64","darwin-arm64","win32-x64"]'), inputs.platform) timeout-minutes: 60 strategy: fail-fast: false matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} + os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-26-xlarge"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-26-xlarge","windows-latest"]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -45,9 +45,9 @@ jobs: timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} + artifact-name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare working-directory: . @@ -69,6 +69,8 @@ jobs: cache-bin: false - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check + - name: Verify release snapshot generation + run: node --test scripts/snapshot-version.test.mjs - if: github.event_name != 'merge_group' && runner.os == 'Linux' run: cargo clippy --all-targets --no-default-features --features test-support,local-runtime,derive -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type # Path-dependency consumers must not rerun build.rs on unchanged rebuilds. @@ -84,78 +86,57 @@ jobs: working-directory: ${{ inputs.sdk-home }}/test/harness - if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - env: + - name: Test out-of-process + id: subprocess + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} RUST_E2E_CONCURRENCY: 4 run: cargo test --no-default-features --features runtime,test-support,derive -- --test-threads=4 --nocapture + - name: Report Linux Rust build resources + if: failure() && runner.os == 'Linux' + run: | + echo "::group::Rust SDK build resources" + df -h . "$RUNNER_TEMP" + df -i . "$RUNNER_TEMP" + free -h + if [ -d target ]; then + du -sh target + fi + echo "::endgroup::" - rust-inprocess: - name: "Rust (${{ matrix.os }}, inprocess)" - if: contains(fromJSON('["all","linux-x64","darwin-arm64"]'), inputs.platform) - strategy: - fail-fast: false - matrix: - os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || '["ubuntu-latest","macos-latest"]') }} - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - working-directory: ${{ inputs.sdk-home }}/rust - env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - COPILOT_SDK_DEFAULT_CONNECTION: inprocess - steps: - - uses: actions/checkout@v7 - timeout-minutes: 4 - with: - persist-credentials: false - - uses: actions/download-artifact@v8 - with: - name: ${{ runner.os == 'Linux' && format('executable-sdk-{0}-{1}-gnu', runner.os, runner.arch) || format('executable-sdk-{0}-{1}', runner.os, runner.arch) }} - path: ${{ github.workspace }} - - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore && node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" prepare - working-directory: . - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: "1.94.0" - - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ inputs.sdk-home }}/rust - prefix-key: v2-sdk-rust-no-bin - cache-bin: false - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/test/harness - - env: + - name: Test in-process smoke + id: inprocess + if: ${{ !cancelled() && (steps.subprocess.outcome == 'success' || steps.subprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: >- cargo test --no-default-features --features test-support,local-runtime,derive --test e2e inprocess::should_start_ping_and_stop_inprocess_client -- --exact --test-threads=1 --nocapture - - env: + - name: Test in-process authentication host + if: ${{ !cancelled() && (steps.inprocess.outcome == 'success' || steps.inprocess.outcome == 'failure') }} + env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + COPILOT_SDK_DEFAULT_CONNECTION: inprocess run: >- cargo test --no-default-features --features test-support,local-runtime,derive --test e2e client::should_use_configured_github_host_for_authentication -- --exact --test-threads=1 --nocapture rust-musl-x64: - name: "Rust (Alpine x64, ${{ matrix.transport }})" + name: "Rust (Alpine x64)" if: inputs.platform == 'all' || inputs.platform == 'linuxmusl-x64' runs-on: ubuntu-latest timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - transport: [default, inprocess] steps: - uses: actions/checkout@v7 timeout-minutes: 4 with: persist-credentials: false - - uses: actions/download-artifact@v8 + - uses: ./.github/actions/download-sdk-runtime with: - name: executable-sdk-Linux-X64-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -170,7 +151,6 @@ jobs: image: rust:1.94.0-alpine sdk-root: /workspace/${{ inputs.sdk-home }} workdir: /workspace/${{ inputs.sdk-home }}/rust - transport: ${{ matrix.transport }} command: | rustc --print cfg | grep -Fqx "target_arch=\"x86_64\"" rustc --print cfg | grep -Fqx "target_env=\"musl\"" @@ -195,15 +175,13 @@ jobs: export CARGO_INCREMENTAL=0 export CARGO_PROFILE_TEST_DEBUG=0 cd "$COPILOT_SDK_ROOT/rust" - if [ "$COPILOT_SDK_TEST_TRANSPORT" = "inprocess" ]; then - unset RUST_E2E_CONCURRENCY - cargo test --no-default-features --features test-support,local-runtime,derive \ - --test e2e inprocess::should_start_ping_and_stop_inprocess_client \ - -- --exact --test-threads=1 --nocapture - cargo test --no-default-features --features test-support,local-runtime,derive \ - --test e2e client::should_use_configured_github_host_for_authentication \ - -- --exact --test-threads=1 --nocapture - else - export RUST_E2E_CONCURRENCY=4 - cargo test --no-default-features --features runtime,test-support,derive -- --test-threads=4 --nocapture - fi + status=0 + RUST_E2E_CONCURRENCY=4 cargo test --no-default-features --features runtime,test-support,derive -- --test-threads=4 --nocapture || status=$? + export COPILOT_SDK_DEFAULT_CONNECTION=inprocess + cargo test --no-default-features --features test-support,local-runtime,derive \ + --test e2e inprocess::should_start_ping_and_stop_inprocess_client \ + -- --exact --test-threads=1 --nocapture || status=$? + cargo test --no-default-features --features test-support,local-runtime,derive \ + --test e2e client::should_use_configured_github_host_for_authentication \ + -- --exact --test-threads=1 --nocapture || status=$? + exit "$status" diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index eff4dbf75e..56c165db89 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -4,6 +4,9 @@ name: "SDK build and test" env: COPILOT_BAZEL_ANG_ENABLED: ${{ vars.COPILOT_BAZEL_ANG_ENABLED }} + COPILOT_BAZEL_ANG_SCOPE: ${{ vars.COPILOT_BAZEL_ANG_SCOPE }} + COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLE_PERCENTAGE }} + COPILOT_BAZEL_ANG_PR_SAMPLED_MODE: ${{ vars.COPILOT_BAZEL_ANG_PR_SAMPLED_MODE }} HUSKY: 0 POWERSHELL_UPDATECHECK: Off SETUP_NODE_TIMEOUT_MINUTES: 10 @@ -24,8 +27,150 @@ permissions: contents: read jobs: + detect-sdk-changes: + name: "Detect SDK-impacting changes" + runs-on: ubuntu-slim + outputs: + has_sdk_changes: ${{ steps.full-coverage.outputs.has_sdk_changes || steps.classify.outputs.has_sdk_changes || steps.recover.outputs.has_sdk_changes }} + steps: + - name: Require full coverage outside pull requests + id: full-coverage + if: github.event_name != 'pull_request' + run: echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + id: checkout + if: github.event_name == 'pull_request' + continue-on-error: true + timeout-minutes: 4 + with: + # Include the synthetic merge commit's base parent without fetching + # repository history. + fetch-depth: 2 + persist-credentials: false + + - name: Detect repository layout + id: repository-layout + if: steps.checkout.outcome == 'success' + continue-on-error: true + run: | + trusted_revision="$(git rev-parse "$GITHUB_SHA^1")" + echo "base-sha=$trusted_revision" >> "$GITHUB_OUTPUT" + if git cat-file -e "${trusted_revision}:src/sdk/package.json" 2>/dev/null && + git cat-file -e "${trusted_revision}:script/sea-build.ts" 2>/dev/null; then + echo "layout=runtime" >> "$GITHUB_OUTPUT" + elif git cat-file -e "${trusted_revision}:package.json" 2>/dev/null && + git cat-file -e "${trusted_revision}:nodejs/package.json" 2>/dev/null; then + echo "layout=standalone" >> "$GITHUB_OUTPUT" + else + echo "::warning::Unable to identify the SDK repository layout; requiring full coverage." + exit 1 + fi + + - name: Find SDK-impacting paths + id: filter + if: steps.repository-layout.outcome == 'success' + continue-on-error: true + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + base_sha: ${{ steps.repository-layout.outputs.base-sha }} + sha: ${{ github.sha }} + skip_initial_fetch: true + fail_on_initial_diff_error: true + quotepath: false + files_yaml: | + runtime: + - .github/actions/** + - .github/workflows/sdk*.yml + - .github/workflows/sdk*.yaml + - .github/workflows/shared-cli-build.yml + - .github/workflows/publish.yml + - .editorconfig + - .gitattributes + - .nvmrc + - package.json + - package-lock.json + - pnpm-lock.yaml + - sdk-protocol-version.json + - assets/** + - BUILD.bazel + - src/** + - generated/** + - schema/** + - files/** + - third_party/** + - patches/** + - tools/bazel/** + - .cargo/** + - bazel/** + - script/** + - .bazelignore + - .bazelrc + - .bazelversion + - Cargo.lock + - Cargo.toml + - MODULE.bazel + - MODULE.bazel.lock + - pnpm-workspace.yaml + - rust-toolchain.toml + - esbuild.ts + - tsconfig*.json + standalone: + - .github/actions/** + - .github/workflows/sdk*.yml + - .github/workflows/sdk*.yaml + - .github/workflows/shared-cli-build.yml + - .github/workflows/publish.yml + - .editorconfig + - .gitattributes + - .nvmrc + - package.json + - package-lock.json + - pnpm-lock.yaml + - sdk-protocol-version.json + - assets/** + - BUILD.bazel + - nodejs/** + - python/** + - go/** + - dotnet/** + - java/** + - rust/** + - scripts/** + - samples/** + - test/** + - docs/** + - README.md + - vitest.config.ts + + - name: Select layout-specific result + id: classify + if: steps.filter.outcome == 'success' + env: + LAYOUT: ${{ steps.repository-layout.outputs.layout }} + RUNTIME_CHANGED: ${{ steps.filter.outputs.runtime_any_modified }} + STANDALONE_CHANGED: ${{ steps.filter.outputs.standalone_any_modified }} + run: | + if [[ "$LAYOUT" == "runtime" ]]; then + has_sdk_changes="$RUNTIME_CHANGED" + elif [[ "$LAYOUT" == "standalone" ]]; then + has_sdk_changes="$STANDALONE_CHANGED" + else + exit 1 + fi + echo "has_sdk_changes=$has_sdk_changes" >> "$GITHUB_OUTPUT" + + - name: Require full coverage after detection errors + id: recover + if: github.event_name == 'pull_request' && (steps.checkout.outcome != 'success' || steps.repository-layout.outcome != 'success' || steps.filter.outcome != 'success' || steps.classify.outcome != 'success') + run: | + echo "::warning::SDK change detection failed; requiring full coverage." + echo "has_sdk_changes=true" >> "$GITHUB_OUTPUT" + detect-layout: name: "Detect SDK layout" + needs: detect-sdk-changes + if: ${{ !cancelled() && (needs.detect-sdk-changes.result != 'success' || needs.detect-sdk-changes.outputs.has_sdk_changes == 'true') }} runs-on: ubuntu-latest outputs: runtime-source: ${{ steps.detect.outputs.runtime-source }} @@ -49,8 +194,9 @@ jobs: fi # TEMPORARY MERGE QUEUE REDUCTION: pull requests and main pushes retain the - # full platform matrix. Merge groups rerun Linux x64 as the representative - # compatibility lane. Remove the merge_group gates below to restore it. + # full SDK matrix. Merge groups rerun only Linux x64 Node.js out-of-process + # CAPI coverage. Remove the merge_group gates here and in sdk-platform.yml + # and sdk-nodejs.yml to restore the full matrix. build-runtime-windows-x64-addons: name: "Build runtime addons (win32-x64)" needs: detect-layout @@ -70,6 +216,8 @@ jobs: with: persist-credentials: false + # Resolve identity before cache restoration and Bazel configuration. The + # composite setup below remains cache-miss-only because it also installs dependencies. - uses: actions/setup-node@v6 with: node-version-file: .nvmrc @@ -89,7 +237,8 @@ jobs: src/native/runtime/index.d.ts src/native/cli/cli-native.win32-x64-msvc.node src/native/cli/index.d.ts - key: windows-x64-addons-v1-release-windows-e2e-no-icf-${{ github.sha }} + # v2 evicts addons created before producer build identity was required. + key: windows-x64-addons-v2-release-windows-e2e-no-icf-${{ github.sha }} - uses: ./.github/actions/setup-node if: steps.cache.outputs.cache-hit != 'true' @@ -124,6 +273,9 @@ jobs: - name: Cross-build Windows x64 addons if: steps.cache.outputs.cache-hit != 'true' env: + # The CI-owned Bazel override is intentionally trusted; without this + # identity the producer stamps an empty source while consumers verify the SHA. + COPILOT_NAPI_ADDONS_BUILD_IDENTITY: ${{ github.sha }} CLI_GIT_COMMIT: ${{ github.sha }} CLI_PACKAGE_NAME: "@github/copilot" CLI_TARGET: win32-x64 @@ -223,6 +375,26 @@ jobs: timeout-minutes: 60 secrets: inherit + build-runtime-linuxmusl-arm64: + name: "Build runtime (linuxmusl-arm64)" + needs: detect-layout + if: github.event_name != 'merge_group' + permissions: + actions: read + contents: read + id-token: write + packages: read + uses: ./.github/workflows/sdk-runtime-artifact.yml + with: + runtime-source: ${{ needs.detect-layout.outputs.runtime-source }} + sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} + os: ubuntu-latest-xl + target: linuxmusl-arm64 + artifact: Linux-ARM64-musl + bazel-config: e2e-no-lto + timeout-minutes: 60 + secrets: inherit + build-runtime-darwin-arm64: name: "Build runtime (darwin-arm64)" needs: detect-layout @@ -236,7 +408,7 @@ jobs: with: runtime-source: ${{ needs.detect-layout.outputs.runtime-source }} sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - os: macos-latest-xlarge + os: macos-26-xlarge target: darwin-arm64 artifact: macOS-ARM64 bazel-config: e2e-no-lto @@ -286,6 +458,16 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit + sdk-linuxmusl-arm64: + name: "Linux musl ARM64 (Java)" + needs: [detect-layout, build-runtime-linuxmusl-arm64] + if: github.event_name != 'merge_group' + uses: ./.github/workflows/sdk-java.yml + with: + platform: linuxmusl-arm64 + sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} + secrets: inherit + sdk-darwin-arm64: name: "macOS ARM64" needs: [detect-layout, build-runtime-darwin-arm64] @@ -307,15 +489,35 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit - sdk-docs: - name: "Documentation" - needs: detect-layout - if: github.event_name != 'merge_group' - uses: ./.github/workflows/sdk-platform.yml - with: - platform: docs - sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - secrets: inherit + # Only Linux CAPI gates this rollup; the full SDK aggregate still reports all coverage. + sdk-typescript: + name: sdk-typescript + if: always() + needs: [detect-sdk-changes, sdk-linux-x64] + runs-on: ubuntu-slim + steps: + - name: Check Linux CAPI Node.js SDK result + env: + DETECT_RESULT: ${{ needs.detect-sdk-changes.result }} + EVENT_NAME: ${{ github.event_name }} + HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }} + RESULTS: ${{ toJSON(needs) }} + run: | + linux_result=$(jq -r '.["sdk-linux-x64"].result' <<< "$RESULTS") + if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then + if [[ "$linux_result" != "skipped" ]]; then + echo "::error::Non-SDK pull request did not skip sdk-linux-x64: $linux_result" + exit 1 + fi + echo "No SDK-impacting changes; Linux CAPI Node.js SDK job skipped" + exit 0 + fi + + result=$(jq -r '.["sdk-linux-x64"].outputs["nodejs-capi-result"]' <<< "$RESULTS") + if [[ "$result" != "success" ]]; then + echo "::error::Linux CAPI Node.js SDK job: expected success, got $result" + exit 1 + fi check-freshness: name: "Check schema and SDK freshness" @@ -398,26 +600,45 @@ jobs: name: "SDK" if: always() needs: + - detect-sdk-changes - build-runtime-windows-x64-addons - build-runtime-linux-x64 - build-runtime-linuxmusl-x64 + - build-runtime-linuxmusl-arm64 - build-runtime-darwin-arm64 - build-runtime-win32-x64 - detect-layout - sdk-linux-x64 - sdk-linuxmusl-x64 + - sdk-linuxmusl-arm64 - sdk-darwin-arm64 - sdk-win32-x64 - - sdk-docs - check-freshness runs-on: ubuntu-slim steps: - name: Check SDK results env: + DETECT_RESULT: ${{ needs.detect-sdk-changes.result }} EVENT_NAME: ${{ github.event_name }} + HAS_SDK_CHANGES: ${{ needs.detect-sdk-changes.outputs.has_sdk_changes }} RESULTS: ${{ toJSON(needs) }} RUNTIME_SOURCE: ${{ needs.detect-layout.outputs.runtime-source }} run: | + if [[ "$EVENT_NAME" == "pull_request" && "$DETECT_RESULT" == "success" && "$HAS_SDK_CHANGES" == "false" ]]; then + unexpected=$(jq -r ' + to_entries[] + | select(.key != "detect-sdk-changes" and .value.result != "skipped") + | "\(.key): \(.value.result)" + ' <<< "$RESULTS") + if [[ -n "$unexpected" ]]; then + echo "::error::Non-SDK pull request did not skip all SDK jobs:" + echo "$unexpected" + exit 1 + fi + echo "No SDK-impacting changes; SDK build and test matrix skipped" + exit 0 + fi + # TEMPORARY MERGE QUEUE REDUCTION: keep this branch aligned with the # gated jobs above until the full merge-group matrix is restored. if [[ "$EVENT_NAME" == "merge_group" ]]; then @@ -430,12 +651,13 @@ jobs: skipped_jobs=( build-runtime-windows-x64-addons build-runtime-linuxmusl-x64 + build-runtime-linuxmusl-arm64 build-runtime-darwin-arm64 build-runtime-win32-x64 sdk-linuxmusl-x64 + sdk-linuxmusl-arm64 sdk-darwin-arm64 sdk-win32-x64 - sdk-docs ) for job in "${required_jobs[@]}"; do if [[ "$(jq -r --arg job "$job" '.[$job].result' <<< "$RESULTS")" != "success" ]]; then @@ -451,7 +673,7 @@ jobs: exit 1 fi done - echo "Linux x64 SDK compatibility and generated clients passed" + echo "Linux x64 Node.js SDK compatibility and generated clients passed" exit 0 fi @@ -466,6 +688,7 @@ jobs: failures=$(jq -r --arg runtime_source "$RUNTIME_SOURCE" ' to_entries[] | select(.value.result != "success") + | select(.key != "detect-sdk-changes") | select( $runtime_source != "published" or .key != "build-runtime-windows-x64-addons" diff --git a/BUILD.bazel b/BUILD.bazel index f3a51c9a39..e25b8ef8e3 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,10 +1,12 @@ SHARED_CODEGEN_INPUTS = [ "nodejs/scripts/releaseArtifacts.ts", "nodejs/src/cliVersion.ts", + "scripts/codegen/legacy-parameters.ts", "scripts/codegen/package-lock.json", "scripts/codegen/package.json", "scripts/codegen/utils.ts", "scripts/runtime-layout.mjs", + "scripts/runtime-release.mjs", ] HAND_WRITTEN_DOTNET_INPUTS = glob( @@ -221,7 +223,9 @@ genrule( "java/scripts/codegen/package-lock.json", "java/scripts/codegen/package.json", "nodejs/package.json", + "scripts/codegen/legacy-parameters.ts", "scripts/runtime-layout.mjs", + "scripts/runtime-release.mjs", ], outs = ["projections/java.tar"], cmd = """ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad9514a939..93cd587a4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,12 +101,21 @@ download missing tools on first use. ### Prepare project dependencies Build tasks restore the selected SDK's dependencies: Node runs -`npm ci --ignore-scripts`, Python runs `uv sync --all-extras --dev`, and the +`npm ci --ignore-scripts --include=dev`, Python runs `uv sync --all-extras --dev`, and the other native build tools restore their project dependencies. In the runtime layout, build/test/generate tasks also install the codegen npm dependencies and refresh public schemas and the selected projections through Bazel. This preparation can update generated source files. +Node SDK builds and codegen preparation explicitly include development +dependencies even with `NODE_ENV=production` or `npm_config_omit=dev`, because +these packages provide the build tools. They skip installation when the npm +arguments, `package.json`, and `package-lock.json` match the last successful install's +`node_modules/.copilot-sdk-install-stamp`. Changing either file, removing +`node_modules` or its stamp, or an unsuccessful install requires a fresh install. +The runtime root's `build:sdk:link` uses the same stamp for Node dependencies. +Java codegen dependencies are installed only when Java is selected. + Tests and checks need additional tools that `pnpm install` does not provide. Before Node, Python, Go, .NET, or Rust SDK tests, prepare Node tooling and the shared replay harness if they are not already installed: @@ -116,6 +125,15 @@ npm --prefix nodejs ci --ignore-scripts npm --prefix test/harness ci --ignore-scripts ``` +Node tests, including the unit-only profile, run generator subprocesses and +require the codegen package's own dependencies. Prepare these before invoking +Node tests directly or from a standalone SDK checkout (runtime build/test tasks +already prepare them): + +```bash +npm --prefix scripts/codegen ci --ignore-scripts +``` + The full Node test task also runs the corrections-script tests: ```bash @@ -133,6 +151,7 @@ Substitute `nodejs`, `python`, `go`, `dotnet`, `java`, or `rust` for ` | From the SDK root | Scope | | --- | --- | | `npm run build:` | Build one SDK; `npm run build` builds all six. | +| `npm run build:default` | Build the Node and Rust SDKs, in that order. | | `npm run test:` | Run that language's suite; `npm test` runs all six. | | `npm run test:default` | Node SDK unit tests and default-feature Rust SDK tests with `test-support`. | | `npm run check:` | Language-specific checks. Java includes `verify` (tests), .NET includes a solution build, and Rust includes Clippy and nightly formatting. | @@ -149,11 +168,17 @@ sources. Runtime-root aliases live in the parent runtime's SDK-local commands live in [package.json](package.json) and [run-tasks.mjs](scripts/run-tasks.mjs). +The runtime root's `pnpm run build` builds the CLI first, then invokes +`pnpm run build:sdk:default`. This default SDK profile prepares Node and Rust +projections together once and does not rebuild the CLI or build the other four SDKs. +Node declaration generation uses TypeScript's incremental build information in +`nodejs/dist/tsconfig.tsbuildinfo`; removing `dist` also clears that state. + In a runtime checkout, SDK tests request a current host `build:cli` before -running; Java and aggregate SDK builds also prepare the CLI. Unchanged Bazel +running; Java and all-six SDK builds also prepare the CLI. Unchanged Bazel actions remain cached. The Rust SDK build uses Bazel, but its tests use the independent SDK Cargo toolchain. Standalone tasks instead use the SDK's pinned -published runtime inputs. Do not work around a missing checkout artifact by +published runtime inputs and build Rust with Cargo's `--all-features`. Do not work around a missing checkout artifact by changing release pins or switching to a published runtime. Rust's `test:rust` and `test:default` run `cargo test --features test-support`; @@ -291,7 +316,12 @@ additional feature/acquisition choices documented in [rust/AGENTS.md](rust/AGENT ### Documentation checks -API snippet validation is separate from SDK tests. From the SDK root: +[SDK CI](.github/workflows/sdk.yml) validates Node.js, Python, Go, .NET, and Java +API snippets after successful tests in their standard Linux jobs (JDK 25 for Java). +These checks run on pull requests, pushes to `main`, and manual workflow runs, +but not merge groups. A documentation failure fails the corresponding language job. + +To validate snippets without running SDK tests, run these commands from the SDK root: ```bash npm --prefix scripts/docs-validation ci @@ -302,8 +332,15 @@ uv run --locked --project python npm run docs:python ``` This extracts and validates snippets for Node.js, Python, Go, .NET, or Java. -Java's current docs validator calls `mvn` directly, -so this task also needs Maven 3.9+ on PATH. There is no `docs:rust` facade; +`docs:java` installs the SDK once through its Maven wrapper, with tests, replay +harness setup, and native downloads disabled, then compiles the snippets using +the same wrapper. CI supplies that installation in its existing post-test step; +the validator does not reinstall it. A separate Maven installation is not required. +Java validation also compiles the exact [README Quick Start](java/README.md#quick-start). +The regular Maven integration suite checks a standalone consumer JAR with a manifest +classpath and runtime dependencies, without a live model, credentials, or native runtime. +These checks replace the former agent-driven Java smoke workflow. +There is no `docs:rust` facade; follow [the Rust SDK workflow](.github/workflows/sdk-rust.yml) for rustdoc. ### Recording and replaying SDK tests @@ -319,6 +356,48 @@ For TypeScript SDK E2Es, use the existing `nodejs/test/e2e/harness/sdkTestContext.ts` fixture. In the runtime repository, also follow the `e2e-test-author` skill's SDK section. +All six SDKs run full subprocess coverage followed by a short in-process +smoke step on the same runner on Linux/glibc x64, macOS ARM64, Windows x64, +and Linux/musl x64. Smoke still runs if the subprocess tests fail, and either +failure fails the job. Java uses JDK 25 on all four platforms, plus a +Linux/glibc JDK 17 compatibility job using precompiled classes. +Merge groups retain the reduced Linux TypeScript CAPI subprocess coverage. + +The `sdk-typescript` required rollup checks only the Linux CAPI job, including +its build, packaging, and applicable static checks. Other platforms, BYOK +backends, and languages keep their existing scheduling and failure reporting; +they do not gate this rollup. The full `SDK` aggregate still requires all +scheduled coverage to succeed. + +The three BYOK backend sweeps run in separate Linux TypeScript jobs, alongside +the normal CAPI job; they do not repeat unit tests, packaging, or static +checks. After preparing the runtime as described above, run a sweep from the +SDK root: + +```bash +COPILOT_SDK_E2E_BACKEND=anthropic-messages GITHUB_ACTIONS=true \ + npm --prefix nodejs test -- test/e2e +``` + +Use `openai-completions` or `openai-responses` for the other sweeps; unset the +variable or use `capi` for the normal suite. BYOK uses the shared captures +through the corresponding protocol adapter and never forwards replay misses +to a live provider. Use the fixture's `createClient()` for secondary clients +that create or resume model-backed sessions. Tests that require CAPI or own +their provider/request-handler setup stay in the normal job, with exclusions +in `vitest.config.ts` or individual `it.skipIf(isByokBackend)` cases. Other SDK +languages retain their language-specific provider tests in their normal suites. + +The Node.js Vitest global setup bundles the shared replay proxy once per run +into Vitest's project-owned temporary directory (and refreshes it on watch +reruns). Each E2E context launches that bundle directly with the test runner's +Node executable, without an npm, shell, or TypeScript-loader subprocess. +Focused Vitest commands use the same setup; no separate proxy build is needed. +If startup or shutdown cleanup cannot confirm that the child exited, `CapiProxy` +retains the child handle so callers can retry cleanup with `stop()`. +Once shutdown is acknowledged, it waits for the child to finish flushing captures +and exit rather than applying a forced-termination timeout to the flush. + Owned-stdio shutdown regressions share `test/harness/stdio-shutdown-runtime.cjs` across all six SDKs. Launch it with Node and arguments ` `. The fixture acknowledges @@ -363,10 +442,12 @@ need copies across languages: Typed cases call the public idiomatic APIs: Node/Zod, C# generics, Python/Pydantic, Go generics, Java annotated records using the existing tool schema generator, -and Rust generics with `derive`/schemars. The tool/follow-up case also checks the -actual provider request's inferred schema, so a recorded JSON response alone -cannot mask missing schema forwarding. Explicit-schema and event-stream cases -exercise the corresponding raw public APIs instead. +and Rust generics with `derive`/schemars. The CAPI leg's tool/follow-up case also +checks the configured OpenAI provider request's inferred schema, so a recorded +JSON response alone cannot mask missing schema forwarding there. BYOK sweeps +retain the typed-result assertions without assuming the same wire-format +encoding. Explicit-schema and event-stream cases exercise the corresponding +raw public APIs instead. The existing suite in each language also checks rejection before admission and zero provider calls for oversized schemas and typed immediate steering. diff --git a/docs/developer-docs/secrets.md b/docs/developer-docs/secrets.md index ff7bd7d79c..b09ddd213c 100644 --- a/docs/developer-docs/secrets.md +++ b/docs/developer-docs/secrets.md @@ -1,23 +1,23 @@ # Secrets management -This document covers secrets management for the github/copilot-sdk repository. It lists the GitHub Actions secrets that maintainers must keep configured and not expired. +This document covers SDK build/test and automation secrets. Normal SDK package publishing runs from `github/copilot-agent-runtime` through its `publish.yml` entry workflow, using runtime-repository credentials and trusted publishers. Curated release notes and Java SNAPSHOT publishing run in the public SDK repository using its credentials. > [!WARNING] > If any of these secrets expire or are revoked, the corresponding workflows will fail silently or with opaque permission errors. Review this list periodically and rotate secrets before they expire. ## SDK test secrets -These secrets are used by the authoritative SDK build/test workflow and the publishing workflow. +These secrets are used by the authoritative SDK build/test workflow. * **`COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY`**: HMAC key used to authenticate with the Copilot Developer CLI integration endpoint during tests. Injected as `COPILOT_HMAC_KEY` in test environments. - * Workflows: `sdk.yml`, `publish.yml` + * Workflows: `sdk.yml` ## Agentic workflow secrets These secrets power the GitHub Agentic Workflows (gh-aw) used for issue triage, code generation, and release automation. * **`COPILOT_GITHUB_TOKEN`**: GitHub OAuth token consumed by the Copilot CLI for AI authentication. Required by all agentic workflows when invoking `copilot` for AI inference. - * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-smoke-test.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` + * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-check.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` * **`GH_AW_GITHUB_TOKEN`**: Optional GitHub token override for repository operations (reading code, creating pull requests, and making GitHub API calls). If unset, workflows use the automatic `GITHUB_TOKEN`. * Workflows: `issue-triage.lock.yml`, `issue-classification.lock.yml`, `handle-bug.lock.yml`, `handle-enhancement.lock.yml`, `handle-question.lock.yml`, `handle-documentation.lock.yml`, `java-codegen-fix.lock.yml`, `java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml`, `release-changelog.lock.yml`, `cross-repo-issue-analysis.lock.yml` @@ -33,19 +33,21 @@ These secrets power the GitHub Agentic Workflows (gh-aw) used for issue triage, ## Java publishing secrets -These secrets support Java SDK Maven Central publishing, snapshot publishing, and post-release documentation deployment. +These secrets support Java SDK Maven Central publishing and post-release documentation deployment from the runtime repository. The SDK-side Java SNAPSHOT workflow uses `JAVA_MAVEN_CENTRAL_USERNAME` and `JAVA_MAVEN_CENTRAL_PASSWORD` in this repository. * **`JAVA_MAVEN_CENTRAL_USERNAME`**: Username generated by a Maven Central Portal user token. - * Workflows: `java-publish-maven.yml`, `java-publish-snapshot.yml` + * Runtime repository (`github/copilot-agent-runtime`): `publish.yml` and `sdk-publish-release.yml` + * SDK repository (`github/copilot-sdk`): `java-publish-snapshot.yml` * **`JAVA_MAVEN_CENTRAL_PASSWORD`**: Password or token for Maven Central (Sonatype OSSRH) authentication. - * Workflows: `java-publish-maven.yml`, `java-publish-snapshot.yml` + * Runtime repository (`github/copilot-agent-runtime`): `publish.yml` and `sdk-publish-release.yml` + * SDK repository (`github/copilot-sdk`): `java-publish-snapshot.yml` * **`JAVA_GPG_SECRET_KEY`**: GPG private key used to sign Java release artifacts for Maven Central. - * Workflows: `java-publish-maven.yml` + * Runtime workflow: `publish.yml` and its reusable SDK publishing jobs * **`JAVA_GPG_PASSPHRASE`**: Passphrase for the GPG signing key. - * Workflows: `java-publish-maven.yml` + * Runtime workflow: `publish.yml` and its reusable SDK publishing jobs * **`JAVA_RELEASE_GITHUB_TOKEN`**: GitHub token with **workflow dispatch** (actions:write) permission on `github/copilot-sdk-java`. Used to trigger the documentation site deployment after a release is published. * Workflows: `publish.yml` @@ -58,10 +60,11 @@ These secrets support Java SDK Maven Central publishing, snapshot publishing, an ## Secrets not managed in this repository * **`GITHUB_TOKEN`**: Automatically provided by GitHub Actions. No manual management required. - The runtime-driven Node SDK workflow grants it `packages: read` only while acquiring - private runtime packages from GitHub Packages. + SDK release packaging consumes runtime artifacts from the same workflow run + instead of acquiring private runtime packages from GitHub Packages. ## Further reading * [GitHub docs: Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) * [Repository secrets settings](https://github.com/github/copilot-sdk/settings/secrets/actions) (maintainer access required) +* [Runtime publishing configuration](https://github.com/github/copilot-agent-runtime/blob/main/docs/developer-docs/secrets.md#unified-publishing-configuration) (runtime repository access required) diff --git a/docs/features/README.md b/docs/features/README.md index 5ea070a5aa..3cf785ecb9 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -11,6 +11,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [The Agent Loop](./agent-loop.md) | How the CLI processes a prompt—the tool-use loop, turns, and completion signals | | [Hooks](./hooks.md) | Intercept and customize session behavior—control tool execution, transform results, handle errors | | [Custom Agents](./custom-agents.md) | Define specialized sub-agents with scoped tools and instructions | +| [Changing Tools](./changing-tools.md) | Replace the tools a client supplies to a live session | | [Fleet Mode](./fleet-mode.md) | Dispatch multiple sub-agents in parallel for large, independent workstreams | | [MCP Servers](./mcp.md) | Integrate Model Context Protocol servers for external tool access | | [Skills](./skills.md) | Load reusable prompt modules from directories | diff --git a/docs/features/changing-tools.md b/docs/features/changing-tools.md new file mode 100644 index 0000000000..93f0938ec9 --- /dev/null +++ b/docs/features/changing-tools.md @@ -0,0 +1,372 @@ +# Changing tools during a session + +An application can replace the tools it supplies to a live session without recreating it. For example, a web page can offer different tools as the user navigates, or a client that takes over a session can bring its own tools. Each SDK replaces the client's complete tool set and the handlers that serve it in one call. + +> **Experimental.** Tool replacement wraps the experimental `session.tools.set` RPC and requires a runtime that supports it. + +## Replace this client's tools + +Pass the complete set of tools this client should supply. It replaces the tools the client supplied when the session was created or resumed, or in an earlier replacement. Built-in, MCP, and plugin tools, and tools that other clients connected to the same session supply, are unaffected. Pass an empty list to remove all of this client's tools. + +Tools are defined exactly as they are for creating a session, and a tool without a handler behaves the same way it does there. + +
+TypeScript + + +```typescript +import { CopilotClient, approveAll, defineTool } from "@github/copilot-sdk"; +import { z } from "zod"; + +declare function findIssues(query: string): Promise; + +async function main() { + const client = new CopilotClient(); + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const searchIssues = defineTool("search_issues", { + description: "Search the issues shown on the current page", + parameters: z.object({ query: z.string().describe("Search text") }), + handler: async ({ query }) => findIssues(query), + }); + + // Supply only the tools the current page provides. + await session.setTools([searchIssues]); + + // Remove all of this client's tools. + await session.setTools([]); +} + +main(); +``` + + +```typescript +import { defineTool } from "@github/copilot-sdk"; +import { z } from "zod"; + +const searchIssues = defineTool("search_issues", { + description: "Search the issues shown on the current page", + parameters: z.object({ query: z.string().describe("Search text") }), + handler: async ({ query }) => findIssues(query), +}); + +// Supply only the tools the current page provides. +await session.setTools([searchIssues]); + +// Remove all of this client's tools. +await session.setTools([]); +``` + +
+
+Python + + +```python +from pydantic import BaseModel, Field + +from copilot import CopilotClient +from copilot.session import PermissionHandler +from copilot.tools import define_tool + + +async def find_issues(query: str) -> str: + return query + + +class SearchIssuesParams(BaseModel): + query: str = Field(description="Search text") + + +@define_tool(description="Search the issues shown on the current page") +async def search_issues(params: SearchIssuesParams) -> str: + return await find_issues(params.query) + + +async def main() -> None: + client = CopilotClient() + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + + # Supply only the tools the current page provides. + await session.set_tools([search_issues]) + + # Remove all of this client's tools. + await session.set_tools([]) +``` + + +```python +from pydantic import BaseModel, Field +from copilot.tools import define_tool + +class SearchIssuesParams(BaseModel): + query: str = Field(description="Search text") + +@define_tool(description="Search the issues shown on the current page") +async def search_issues(params: SearchIssuesParams) -> str: + return await find_issues(params.query) + +# Supply only the tools the current page provides. +await session.set_tools([search_issues]) + +# Remove all of this client's tools. +await session.set_tools([]) +``` + +
+
+Go + + +```go +package main + +import ( + "context" + "log" + + copilot "github.com/github/copilot-sdk/go" +) + +type SearchIssuesParams struct { + Query string `json:"query" jsonschema:"Search text"` +} + +func findIssues(query string) (string, error) { + return query, nil +} + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + log.Fatal(err) + } + + searchIssues := copilot.DefineTool( + "search_issues", + "Search the issues shown on the current page", + func(params SearchIssuesParams, inv copilot.ToolInvocation) (string, error) { + return findIssues(params.Query) + }, + ) + + // Supply only the tools the current page provides. + if err := session.SetTools(ctx, []copilot.Tool{searchIssues}); err != nil { + log.Fatal(err) + } + + // Remove all of this client's tools. + if err := session.SetTools(ctx, nil); err != nil { + log.Fatal(err) + } +} +``` + + +```go +type SearchIssuesParams struct { + Query string `json:"query" jsonschema:"Search text"` +} + +searchIssues := copilot.DefineTool( + "search_issues", + "Search the issues shown on the current page", + func(params SearchIssuesParams, inv copilot.ToolInvocation) (string, error) { + return findIssues(params.Query) + }, +) + +// Supply only the tools the current page provides. +if err := session.SetTools(ctx, []copilot.Tool{searchIssues}); err != nil { + log.Fatal(err) +} + +// Remove all of this client's tools. +if err := session.SetTools(ctx, nil); err != nil { + log.Fatal(err) +} +``` + +
+
+.NET + + +```csharp +#pragma warning disable GHCP001 +using System.ComponentModel; +using GitHub.Copilot; +using Microsoft.Extensions.AI; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + OnPermissionRequest = PermissionHandler.ApproveAll, +}); + +var searchIssues = CopilotTool.DefineTool( + ([Description("Search text")] string query) => FindIssues(query), + factoryOptions: new AIFunctionFactoryOptions + { + Name = "search_issues", + Description = "Search the issues shown on the current page", + }); + +// Supply only the tools the current page provides. +await session.SetToolsAsync([searchIssues]); + +// Remove all of this client's tools. +await session.SetToolsAsync([]); + +static string FindIssues(string query) => query; +#pragma warning restore GHCP001 +``` + + +```csharp +var searchIssues = CopilotTool.DefineTool( + ([Description("Search text")] string query) => FindIssues(query), + factoryOptions: new AIFunctionFactoryOptions + { + Name = "search_issues", + Description = "Search the issues shown on the current page", + }); + +// Supply only the tools the current page provides. +await session.SetToolsAsync([searchIssues]); + +// Remove all of this client's tools. +await session.SetToolsAsync([]); +``` + +`SetToolsAsync` raises the `GHCP001` experimental diagnostic, which you suppress with `#pragma warning disable GHCP001` or a project-level `GHCP001`. + +
+
+Java + + +```java +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotClient; +import com.github.copilot.CopilotSession; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.ToolDefinition; + +@AllowCopilotExperimental +public class ChangingToolsExample { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + var searchIssues = ToolDefinition.create( + "search_issues", + "Search the issues shown on the current page", + Map.of( + "type", "object", + "properties", Map.of("query", Map.of("type", "string", "description", "Search text")), + "required", List.of("query")), + invocation -> CompletableFuture.completedFuture( + findIssues((String) invocation.getArguments().get("query")))); + + // Supply only the tools the current page provides. + session.setTools(List.of(searchIssues)).get(); + + // Remove all of this client's tools. + session.setTools(List.of()).get(); + } + } + + private static String findIssues(String query) { + return query; + } +} +``` + + +```java +var searchIssues = ToolDefinition.create( + "search_issues", + "Search the issues shown on the current page", + Map.of( + "type", "object", + "properties", Map.of("query", Map.of("type", "string", "description", "Search text")), + "required", List.of("query")), + invocation -> CompletableFuture.completedFuture( + findIssues((String) invocation.getArguments().get("query")))); + +// Supply only the tools the current page provides. +session.setTools(List.of(searchIssues)).get(); + +// Remove all of this client's tools. +session.setTools(List.of()).get(); +``` + +`setTools` is `@CopilotExperimental`, so the consuming class or method must opt in with `@AllowCopilotExperimental`. See [Using experimental APIs](../../java/README.md#using-experimental-apis). + +
+
+Rust + + + +```rust +#[derive(Deserialize, JsonSchema)] +struct SearchIssuesParams { + /// Search text + query: String, +} + +let search_issues = define_tool( + "search_issues", + "Search the issues shown on the current page", + |_inv, params: SearchIssuesParams| async move { + Ok(ToolResult::Text(find_issues(¶ms.query).await)) + }, +); + +// Supply only the tools the current page provides. +session.set_tools(vec![search_issues]).await?; + +// Remove all of this client's tools. +session.set_tools(Vec::new()).await?; +``` + +
+ +## When the new tools take effect + +- **Model requests.** The runtime offers the new tools from the agent's next model request. That request can be part of a turn that is already in progress. +- **Handlers.** As soon as the runtime accepts the replacement, every tool call the session dispatches uses the new handlers. Calls that are already running finish on the handlers that started them. +- **Rejection.** If the runtime rejects the replacement, the call fails and the previous tools and handlers stay in place. The runtime rejects tool names with invalid characters and names that another connected client already supplies. +- **Ordering.** Concurrent replacements on the same session are applied one at a time, in the order they're made. +- **Cancellation.** In SDKs that let you cancel the call, cancelling it while an earlier replacement is still in flight sends nothing. Once the request is sent, cancelling only stops the wait: if the runtime accepts the replacement, the new handlers still take effect. + +## Replacing tools during a turn + +A model request that is already in flight was made with the previous tools, so the agent can still call a tool you just removed. The session doesn't answer that call, because another connected client might supply a tool with the same name, so the call can stay pending until the turn is aborted. + +If a running turn might still call a tool you remove, replace tools while the session is idle, for example after the `session.idle` event, or abort the turn first. Replacing a tool's handler under the same name is safe during a turn: calls dispatched after the runtime accepts the replacement use the new handler. + +## SDK reference + +| SDK | Method | +|---|---| +| TypeScript | `session.setTools(tools)` | +| Python | `await session.set_tools(tools)` | +| Go | `session.SetTools(ctx, tools)` | +| .NET | `await session.SetToolsAsync(tools, cancellationToken)` | +| Java | `session.setTools(tools)`, which returns a `CompletableFuture` | +| Rust | `session.set_tools(tools).await` | diff --git a/docs/features/fleet-mode.md b/docs/features/fleet-mode.md index 891a8ef04f..9a9547c8b9 100644 --- a/docs/features/fleet-mode.md +++ b/docs/features/fleet-mode.md @@ -243,7 +243,9 @@ This pattern gives every worker a clear owner and lets the parent session reason Fleet mode invokes sub-agents through the runtime's task mechanism. The runtime emits hook activity for sub-agent tool calls: the runtime 1.0.52 changelog notes that `preToolUse`, `postToolUse`, `subagentStart`, and `subagentStop` fire correctly for sub-agent tool calls. -A dedicated SDK hook callback for `subagentStart` or `subagentStop` was not found in the public SDK surface on this branch. SDK consumers can observe sub-agent activity through the generic session event stream, which includes events such as `subagent.started`, `subagent.completed`, `subagent.failed`, `subagent.selected`, and `subagent.deselected`. +All six SDKs expose typed sub-agent lifecycle hooks: Node.js uses `onSubagentStart` and `onSubagentStop`, Python and Rust use `on_subagent_start` and `on_subagent_stop`, Go and .NET use `OnSubagentStart` and `OnSubagentStop`, and Java uses `setOnSubagentStart` and `setOnSubagentStop`. Register them when creating or resuming a session. The start hook can prepend `additionalContext` to the child's first prompt; the stop hook can request another child turn with a block reason or replace the response reported to the parent. + +For passive status updates, subscribe to the generic session event stream. It includes `subagent.started`, `subagent.completed`, `subagent.failed`, `subagent.selected`, and `subagent.deselected`:
Node.js / TypeScript @@ -338,7 +340,7 @@ Keep plugin-provided sub-agent types narrow and descriptive so the orchestrator * Fleet mode is exposed through generated session RPC bindings and is marked experimental in several SDKs. * The SQL todos pattern is the canonical coordination model in the runtime guidance, but whether it is a stable extensibility contract for SDK consumers is still an open question. -* `subagentStart` and `subagentStop` are runtime hook names; this branch exposes sub-agent lifecycle to SDK consumers through the generic session event stream, not dedicated hook callbacks. +* Sub-agent lifecycle hook inputs identify the parent session but do not yet carry a shared tool-call ID for correlating with `subagent.*` events. * Plugin sub-agent registration is configured at the runtime layer through `--plugin-dir`; no SDK-level plugin registration helper was verified on this branch. * Java native typed bindings for `session.fleet.start` were not found in the Java SDK source on this branch. * Fleet mode does not remove the need for parent-agent review. Parallel workers can produce inconsistent assumptions that the orchestrator must reconcile. diff --git a/docs/features/mcp.md b/docs/features/mcp.md index 19f12e285b..cf3003ce8c 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -183,6 +183,57 @@ On session creation and a **cold** resume, disabled servers are not started and the runtime does not initiate their authentication. A resident resume cannot undo a server that the runtime has already spawned. Names are matched exactly. +## Listing and retrieving prompts + +The experimental generated MCP namespace exposes the wire JSON-RPC methods +`session.mcp.prompts.list` and `session.mcp.prompts.get`. Use the language-specific +SDK accessors below to call them. These methods target one connected server in the +current session; they do not combine results from multiple servers. + +| SDK | List prompts | Get a prompt | +| --- | --- | --- | +| Node.js | `session.rpc.mcp.prompts.list(...)` | `session.rpc.mcp.prompts.get(...)` | +| Python | `session.rpc.mcp.prompts.list(...)` | `session.rpc.mcp.prompts.get(...)` | +| Go | `session.RPC.MCP.Prompts().List(...)` | `session.RPC.MCP.Prompts().Get(...)` | +| .NET | `session.Rpc.Mcp.Prompts.ListAsync(...)` | `session.Rpc.Mcp.Prompts.GetAsync(...)` | +| Java | `session.getRpc().mcp.prompts.list(...)` | `session.getRpc().mcp.prompts.get(...)` | +| Rust | `session.rpc().mcp().prompts().list(...)` | `session.rpc().mcp().prompts().get(...)` | + +Listing requires `serverName` and accepts an optional opaque `cursor`. Each +call returns one page of typed prompt definitions and an optional `nextCursor`. +Pass that cursor in another list call to request the next page. Definitions +include names, optional titles and descriptions, and argument definitions. +An omitted argument `required` flag remains distinct from `false`. + +Getting a prompt requires `serverName` and `promptName`, with an optional +`arguments` dictionary whose values are strings. Omitting `arguments` leaves the +MCP request's arguments absent; passing `{}` sends an explicitly empty dictionary. +The runtime preserves this distinction for the server. The result contains an optional +description and ordered messages with typed roles and opaque JSON `content`. +Inspect each content block's `type` before interpreting it; content is not +flattened into text. Nested resources, annotations, metadata, unfamiliar content +types, and additional content fields retain their JSON structure. Server +extensions on typed descriptors, messages, and result envelopes are available under +`additionalProperties`. A server extension itself named `additionalProperties` +is preserved as an entry inside that map, not merged into it. +The protocol's top-level `resultType: "complete"` discriminator is consumed by +the runtime; it is not returned as an SDK field or a server extension. +Requests fail explicitly if the runtime cannot preserve the raw response, +including stdio response frames exceeding the 1 MiB capture limit. + +The returned prompt messages are not automatically sent to the model or used to +execute tools or fetch referenced resources. Your application decides how to +use the result. +While generating that result, an MCP server can request sampling or elicitation +through multi round-trip continuations. These requests use the same configured +host responders as other MCP operations; prompt retrieval does not grant +additional permission or bypass the host's decision. A missing responder or +responder error fails the request, and an elicitation decline is returned to the +server unchanged. State-only continuations do not invoke either responder. + +After an existing `mcp.prompts.list_changed` session event, list the named +server's prompts again to refresh your application's view. + ## Tool configuration You can control which tools are available to an MCP server using the `tools` field. @@ -295,6 +346,53 @@ directories for different applications. | `tools` | `string[]` | No | Tools to enable | | `timeout` | `number` | No | Timeout in milliseconds | +## OAuth ownership for remote servers + +Choose one OAuth ownership model for each authentication attempt: + +1. **Runtime-managed loopback**: Call `session.mcp.oauth.login` without a + redirect URI. The runtime opens a local callback listener and owns discovery, + PKCE, state validation, token exchange, persistence, refresh, and reconnect. +1. **Runtime-managed hosted callback**: Call `session.mcp.oauth.login` with a + trusted public HTTPS redirect URI. Your host receives the callback, but the + runtime continues to own the OAuth protocol and credentials. +1. **Host-managed OAuth**: Use the SDK's MCP authentication request handler and + return a host-acquired access token. Your host owns authorization and refresh. + +Do not register a host-managed OAuth handler for an authentication attempt that +uses a runtime-managed hosted callback. Wait until the remote server enters the +`needs-auth` state, then call the generated `session.rpc.mcp.oauth.login` method +directly with the server name and callback URI: + +```jsonc +{ + "serverName": "remote-mcp", + "redirectUri": "https://agent.example.com/oauth/callback" +} +``` + +The selected static, CIMD, or dynamically registered OAuth client must advertise +that exact URI. The URI must use HTTPS and must not contain a query or fragment. +When browser interaction is required, the result contains `authorizationUrl` +and `authorizationId`. Open `authorizationUrl` in the user's browser and retain +`authorizationId` with the target session. + +After the authorization server redirects to your endpoint, call the generated +`session.rpc.mcp.oauth.complete` method: + +```jsonc +{ + "authorizationId": "", + "callbackUrl": "https://agent.example.com/oauth/callback?code=...&state=..." +} +``` + +Construct `callbackUrl` from the configured public redirect origin and path plus +the callback's original query. Do not pass an internal service URL or trust +client-supplied `Forwarded` or `X-Forwarded-*` headers. The runtime validates the +origin, port, path, state, and OAuth response before accepting delivery. Token +exchange, persistence, and MCP reconnect then continue asynchronously. + ## Session-scoped MCP diagnostics Set `diagnostics: { sources: { mcp: { level: "debug" } } }` when you create or diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 69fda41fba..302844b347 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -131,6 +131,30 @@ await session.SendAndWaitAsync(new MessageOptions { Prompt = "Analyze my codebas Later—minutes, hours, or even days—you can resume the session from where you left off. +### Transcript recovery + +The resume option `allowTranscriptRecovery` controls recovery when the runtime loads a +transcript from storage. It defaults to `true` in all modes. With `false`, a load that +would discard damaged records or move `session.start` fails with JSON-RPC error `-32075` +without rewriting the transcript. An intact final record without a newline is accepted. + +Records skipped for forward compatibility still count when checking persisted order. +If such records precede `session.start`, recovery must move the start record ahead of +them. A transcript containing only skipped records is not empty and cannot be recovered +without a valid `session.start`. + +When recovery is allowed, the returned session exposes `transcriptRecovery`, including +the invalid line numbers, whether `session.start` moved, and a planned byte-exact backup +path. Loading alone does not write that backup; the next durable append performs the +repair and backup. + +> [!NOTE] +> This option applies to a new transcript load, not to the lifetime of a session. +> Reconnecting to a session already resident in the runtime reuses its live history +> without revalidating storage or rejecting recovery authorized by an earlier resume. +> The recovery report describes the load that performed recovery, not every subsequent +> reconnect. + ```mermaid flowchart LR subgraph Day1["Day 1"] diff --git a/docs/features/skills.md b/docs/features/skills.md index 30c5112dbd..df8d319928 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -336,6 +336,333 @@ The frontmatter fields: The markdown body contains the instructions that are injected into the session context when the skill is loaded. +## Skill providers (experimental) + +> [!NOTE] +> Skill providers are experimental. The API can change in future SDK releases. + +A skill provider serves skills from your application's own storage, such as a database in a multi-tenant service, instead of `SKILL.md` files on disk. Provider skills join the session's skill catalog alongside file-based skills. The model loads them on demand through the `skill` tool, and users can invoke them like any other skill. + +A provider implements two operations: + +* **List skills**: returns catalog metadata for every skill: a `name`, a `description`, and optionally `userInvocable`, `disableModelInvocation`, and `argumentHint`. The runtime calls it when it loads the session's skills. +* **Read skill**: returns the markdown for one skill, or a not-found result if the skill no longer exists. The runtime calls it only when the skill is loaded. + +Pass the provider when you create or resume a session: + +
+Node.js / TypeScript + +```typescript +import { approveAll, CopilotClient, type SkillProvider } from "@github/copilot-sdk"; + +const releaseSkills = new Map([ + [ + "release-notes", + { + description: "Writes release notes in the team's format.", + markdown: "Group changes by feature area and link each pull request.", + }, + ], +]); + +const skillProvider: SkillProvider = { + listSkills: () => + [...releaseSkills].map(([name, skill]) => ({ name, description: skill.description })), + readSkill: (name) => releaseSkills.get(name)?.markdown ?? null, +}; + +const client = new CopilotClient(); +const session = await client.createSession({ + onPermissionRequest: approveAll, + skillProvider, +}); +``` + +
+
+Python + +```python +from copilot import CopilotClient, SkillProviderDescriptor +from copilot.session import PermissionHandler + +RELEASE_SKILLS = { + "release-notes": ( + "Writes release notes in the team's format.", + "Group changes by feature area and link each pull request.", + ), +} + + +class ReleaseSkills: + async def list_skills(self) -> list[SkillProviderDescriptor]: + return [ + SkillProviderDescriptor(name=name, description=description) + for name, (description, _) in RELEASE_SKILLS.items() + ] + + async def read_skill(self, name: str) -> str | None: + skill = RELEASE_SKILLS.get(name) + return skill[1] if skill else None + + +async def main(): + client = CopilotClient() + await client.start() + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_provider=ReleaseSkills(), + ) +``` + +
+
+Go + +```go +package main + +import ( + "context" + "fmt" + "log" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + +type releaseSkill struct { + description string + markdown string +} + +type releaseSkills map[string]releaseSkill + +func (s releaseSkills) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + descriptors := make([]rpc.SkillProviderDescriptor, 0, len(s)) + for name, skill := range s { + descriptors = append(descriptors, rpc.SkillProviderDescriptor{Name: name, Description: skill.description}) + } + return descriptors, nil +} + +func (s releaseSkills) ReadSkill(ctx context.Context, name string) (string, error) { + skill, ok := s[name] + if !ok { + return "", fmt.Errorf("%w: %s", copilot.ErrSkillNotFound, name) + } + return skill.markdown, nil +} + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: releaseSkills{ + "release-notes": { + description: "Writes release notes in the team's format.", + markdown: "Group changes by feature area and link each pull request.", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + _ = session +} +``` + +
+
+.NET + +```csharp +#pragma warning disable GHCP001 // Skill providers are experimental. +using GitHub.Copilot; +using GitHub.Copilot.Rpc; + +public sealed class ReleaseSkills : ISkillProvider +{ + private readonly Dictionary _skills = new() + { + ["release-notes"] = ( + "Writes release notes in the team's format.", + "Group changes by feature area and link each pull request."), + }; + + public Task> ListSkillsAsync(CancellationToken cancellationToken) => + Task.FromResult>( + _skills.Select(skill => new SkillProviderDescriptor + { + Name = skill.Key, + Description = skill.Value.Description, + }).ToList()); + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(_skills.TryGetValue(name, out var skill) ? skill.Markdown : null); +} + +public static class SkillProviderExample +{ + public static async Task RunAsync(CopilotClient client) + { + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SkillProvider = new ReleaseSkills(), + }); + } +} +``` + +The .NET skill provider types raise the `GHCP001` experimental diagnostic. Suppress it with `#pragma warning disable GHCP001` or a project-level `GHCP001`. + +
+
+Java + +```java +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotClient; +import com.github.copilot.CopilotSession; +import com.github.copilot.SkillProvider; +import com.github.copilot.SkillProviderDescriptor; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@AllowCopilotExperimental +class ReleaseSkills implements SkillProvider { + private final Map markdown = Map.of( + "release-notes", "Group changes by feature area and link each pull request."); + + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of(new SkillProviderDescriptor( + "release-notes", "Writes release notes in the team's format.", null, null, null))); + } + + @Override + public CompletableFuture readSkill(String name) { + return CompletableFuture.completedFuture(markdown.get(name)); + } +} + +@AllowCopilotExperimental +class SkillProviderExample { + static CopilotSession createSession(CopilotClient client) throws Exception { + return client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillProvider(new ReleaseSkills())) + .get(); + } +} +``` + +The Java skill provider API is `@CopilotExperimental`, so the consuming class or method must opt in with `@AllowCopilotExperimental` (or compile with `-Acopilot.experimental.allowed=true`). See [Using experimental APIs](../../java/README.md#using-experimental-apis). + +
+
+Rust + + +```rust +use std::sync::Arc; + +use async_trait::async_trait; +use github_copilot_sdk::{Error, SessionConfig, SkillProvider, SkillProviderDescriptor}; + +struct ReleaseSkills; + +#[async_trait] +impl SkillProvider for ReleaseSkills { + async fn list_skills(&self) -> Result, Error> { + Ok(vec![SkillProviderDescriptor { + name: "release-notes".into(), + description: "Writes release notes in the team's format.".into(), + ..Default::default() + }]) + } + + async fn read_skill(&self, name: &str) -> Result, Error> { + Ok((name == "release-notes") + .then(|| "Group changes by feature area and link each pull request.".to_string())) + } +} + +let session = client + .create_session( + SessionConfig::default() + .approve_all_permissions() + .with_skill_provider(Arc::new(ReleaseSkills)), + ) + .await?; +``` + +
+ +### Provider skill content + +Read skill returns the skill's `SKILL.md` text. YAML frontmatter is optional: + +* Without frontmatter, the whole text is the skill body, and all metadata comes from the listed skill. +* With frontmatter, an omitted field inherits the listed value. A field that you include must match the listed value, or the skill fails to load. +* `allowed-tools` is read only from frontmatter. +* If the first line of the text is `---`, the runtime parses it as frontmatter, so don't start a body-only skill with a Markdown thematic break. + +Provider skills are text-only. They have no base directory, so they can't reference bundled scripts, templates, or other files. `skills.list` reports them with the source `sdk` and an empty `path`. + +### Serving existing SKILL.md files + +To serve `SKILL.md` files that you already have, such as files stored in a database, parse each file's YAML frontmatter once to build its catalog entry, then return the file unchanged from read skill: + +* Map `name` and `description` to the listed skill's `name` and `description`. Both are required in the catalog; the runtime doesn't fall back to a folder name or the skill body. +* Map `user-invocable`, `disable-model-invocation`, and `argument-hint` to the matching optional fields when the frontmatter sets them. +* Return the original text, frontmatter included, from read skill. Because the listed values came from the same frontmatter, they match, and the runtime still reads `allowed-tools` from it. + +### Trusting provider content + +Treat provider skills like skill directories: their content is trusted input. The model follows a skill's instructions, and the frontmatter controls how the skill is invoked. `allowed-tools` doesn't grant permissions in SDK sessions. The runtime reports it in the `allowedTools` field of the `skill.invoked` event, and your permission handler still decides every tool request. + +Don't serve text that end users or other tenants can edit unless you would let them author a skill file. If you build skills from user input, generate the frontmatter yourself instead of passing user-supplied frontmatter through. + +### Provider limits and errors + +The runtime validates the provider's catalog and content: + +* Names must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` and be unique, ignoring case. +* A catalog can contain at most 1,024 skills and 1 MiB of metadata. Each description and argument hint can be at most 1,024 characters. +* Each skill's markdown can be at most 1 MiB of UTF-8. +* Each call must complete within 30 seconds. When the runtime stops waiting for a call, because it timed out, the session ended, or a resume replaced the provider, it cancels the request and ignores any later result. Each SDK passes that cancellation to your provider in its usual way: + + | SDK | Cancellation signal | + |---|---| + | Node.js | The `signal` (`AbortSignal`) in the options passed to `listSkills` and `readSkill` is aborted. | + | Python | The provider's `asyncio` task is cancelled with `asyncio.CancelledError`. Synchronous providers run to completion. | + | Go | The call's `context.Context` is cancelled. | + | .NET | The call's `CancellationToken` is cancelled. | + | Java | The returned `CompletableFuture` is cancelled with `cancel(true)`, which doesn't interrupt running work. | + | Rust | The provider future is dropped. | + + These signals cover calls that the runtime cancels. When the connection closes or the client is force-stopped, the .NET, Java, and Rust SDKs also cancel calls that are still running. The Node.js, Python, and Go SDKs don't: a running call continues until it returns, and the SDK discards its result. + +If listing fails or returns an invalid catalog, the runtime reports the problem in the `errors` list returned by `skills.reload`. A failed reload keeps the provider skills from the last successful list. If reading a skill fails, the `skill` tool reports a generic load failure to the model. The SDK never forwards the text of errors your provider throws or returns, so that text can't leak into the conversation. + +### Provider lifecycle + +A provider is bound to one session: + +* A provider enables skills unless you set `enableSkills` to `false`, which keeps the provider bound but never called. In `mode: "empty"`, skills stay disabled until you set `enableSkills` to `true`. +* The provider is never persisted. Pass it again when you resume a session. Resuming without a provider removes it from the session. Extensions that join the session don't change it. +* A provider skill with the same name as a file-based skill replaces the file-based skill, and `skills.reload` reports a warning. +* Sub-agents use their parent session's provider. +* The runtime can call the provider concurrently, for example from the `skill` tool, user invocations, and custom agents that preload skills, so both operations must be safe for concurrent use. +* Cloud sessions don't support skill providers. The SDK rejects the configuration before it creates the session. + ## Configuration options ### SessionConfig skill fields @@ -344,12 +671,18 @@ The markdown body contains the instructions that are injected into the session c |----------|-------|------|-------------| | Node.js | `skillDirectories` | `string[]` | Directories to load skills from | | Node.js | `disabledSkills` | `string[]` | Skills to disable | +| Node.js | `skillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | Python | `skill_directories` | `list[str]` | Directories to load skills from | | Python | `disabled_skills` | `list[str]` | Skills to disable | +| Python | `skill_provider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | Go | `SkillDirectories` | `[]string` | Directories to load skills from | | Go | `DisabledSkills` | `[]string` | Skills to disable | +| Go | `SkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | | .NET | `SkillDirectories` | `List` | Directories to load skills from | | .NET | `DisabledSkills` | `List` | Skills to disable | +| .NET | `SkillProvider` | `ISkillProvider` | Experimental provider for SDK-supplied skills | +| Java | `setSkillProvider` | `SkillProvider` | Experimental provider for SDK-supplied skills | +| Rust | `with_skill_provider` | `Arc` | Experimental provider for SDK-supplied skills | ### Built-in skills and `mode: "empty"` diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 0292f98e00..dbb6111897 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -532,12 +532,31 @@ Emitted when a tool begins executing. ### `tool.execution_partial_result` -Ephemeral. Incremental output from a running tool (e.g., streaming bash output). +Deprecated. Use `tool.shell_output` for live terminal output. This ephemeral event remains emitted alongside the replacement during migration; subscribe to only one representation to avoid displaying the same output twice. | Data Field | Type | Required | Description | |------------|------|----------|-------------| | `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` | -| `partialOutput` | `string` | ✅ | Incremental output chunk | +| `partialOutput` | `string` | ✅ | Bounded, cumulative replacement snapshot with stdout and stderr merged; not an append-only chunk | + +### `tool.shell_output` + +Ephemeral. Append-only output from an attached terminal command or a user-requested terminal command. Each event contains newly observed, secret-filtered text, without the separators or markers used to format final tool results for the model. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Identifies the originating tool call | +| `text` | `string` | ✅ | New text to append, not a replacement snapshot | +| `stream` | `"stdout" \| "stderr" \| "terminal"` | | Output source; omitted means `"stdout"`. `"terminal"` identifies inherently merged PTY output, whose stdout/stderr origin cannot be recovered | +| `sequence` | `number` | ✅ | Starts at zero and increases across all streams for this tool call, in observed publication order | + +Accumulate `text` separately by `toolCallId` and `stream`, or join the chunks in sequence order for a combined display. A sequence number identifies duplicates and missing chunks; it does not establish exact ordering between independent operating-system writes. A late subscriber can receive a first sequence greater than zero. + +These events are live-only: they are not persisted or replayed after resume. Final `tool.execution_complete` results, model-facing formatting, and detached command capture are unchanged. Text can contain terminal control sequences; renderers must apply their normal terminal-text handling. + +An attached terminal command can keep running after `tool.execution_complete` when it runs asynchronously or continues beyond `initial_wait`. Output received while it continues uses the original `toolCallId` and continues the same `sequence`, rather than restarting at zero. Consumers that display only active calls must ignore chunks received after completion instead of recreating completed-call state. + +Secret filtering operates on each decoded chunk. The runtime does not artificially split lines, but operating-system reads can divide a line or secret across chunks; cross-chunk secret reconstruction is not provided. ### `tool.execution_progress` @@ -837,7 +856,7 @@ A skill was activated for the current conversation. | `name` | `string` | ✅ | Skill name | | `path` | `string` | ✅ | File path to the SKILL.md definition | | `content` | `string` | ✅ | Full skill content injected into the conversation | -| `allowedTools` | `string[]` | | Tools auto-approved while this skill is active | +| `allowedTools` | `string[]` | | Tools listed in the skill's `allowed-tools` frontmatter. The SDK doesn't approve them automatically; your permission handler still decides. The Copilot CLI auto-approves them while the skill is active. | | `pluginName` | `string` | | Plugin the skill originated from | | `pluginVersion` | `string` | | Plugin version | @@ -970,7 +989,7 @@ assistant.turn_start → Turn begins │ ├── permission.requested → Needs user approval │ ├── permission.completed → Approval result │ ├── tool.execution_start → Tool begins -│ ├── tool.execution_partial_result → Streaming tool output (ephemeral, repeated) +│ ├── tool.shell_output → Stream-tagged terminal output (ephemeral, repeated) │ ├── tool.execution_progress → Progress updates (ephemeral, repeated) │ ├── tool.execution_complete → Tool finished │ │ @@ -998,6 +1017,7 @@ This table lists key `data` payload fields. Common envelope fields are documente | `tool.user_requested` | | Tool | `toolCallId`, `toolName`, `arguments?` | | `tool.execution_start` | | Tool | `toolCallId`, `toolName`, `arguments?`, `mcpServerName?` | | `tool.execution_partial_result` | ✅ | Tool | `toolCallId`, `partialOutput` | +| `tool.shell_output` | ✅ | Tool | `toolCallId`, `text`, `stream?`, `sequence` | | `tool.execution_progress` | ✅ | Tool | `toolCallId`, `progressMessage` | | `tool.execution_complete` | | Tool | `toolCallId`, `success`, `result?`, `error?` | | `session.idle` | ✅ | Session | `aborted?` | diff --git a/docs/getting-started.md b/docs/getting-started.md index 5512da8531..b6172d6fc8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1843,6 +1843,14 @@ Copilot decides when to call your tool based on the user's question. When it doe 1. The result is sent back to Copilot 1. Copilot incorporates the result into its response +### Updating external tools in a running session + +The existing low-level `session.tools.set` request replaces the definitions owned by that SDK connection. Send the complete desired list (or an empty list to remove them), rather than a patch. The updated definitions apply to the next model request that reads them. During a tool round, a change processed before the post-tool refresh is visible on the next model call; a concurrent update processed after that refresh takes effect on a later request. It does **not** update the high-level SDK tool-handler map: register and manage handlers separately, or handle `external_tool.requested` and complete it with `session.tools.handlePendingToolCall`. + +On direct Anthropic API connections to `https://api.anthropic.com` using a model marked for Anthropic tool updates in the runtime's model catalog (currently Claude Fable 5 or 5.1, or Claude Opus 4.8, 5, or 5.5), the runtime can keep the original tool prefix and represent eligible between-turn SDK changes in provider-native conversation items to preserve a cacheable prefix. On the CAPI WebSocket Responses route for a model marked for Responses tool updates in that catalog (currently GPT-5.4 and 5.4 Mini, GPT-5.5, GPT-5.6 Luna, Sol, Terra, and Sol-fast, GPT-6 Astra, Sol, and Luna, and GPT-6.1 Sol), eligible append-only additions can use positioned `additional_tools` items. Eligible removals retain historical definitions and restrict callable tools with `tool_choice: { type: "allowed_tools", ... }`, or `"none"` when no tools remain. This removal optimization requires every callable survivor to be present in the original top-level tool list: CAPI rejects allowlist references to inline-added definitions when full history is resent. Removing an inline-added tool can preserve its historical position, but retaining a callable inline-added tool while removing another tool falls back to the complete current list. Schema replacements and unsupported tool-choice or provider-native tool configurations also use that fallback. + +Model support is an explicit catalog opt-in, independent of support for reasoning configuration updates, and other models should be enabled only after their route is verified. If historical changes no longer fit the prompt budget, or an optimized request receives a recognized pre-output provider rejection, the runtime returns to the current complete tool list. Anthropic through CAPI, unannotated OpenAI Responses models, other provider endpoints, mid-turn changes, and history rewrites use the current complete tool list. Removing every callable tool selects the runtime's existing single-shot HTTP path, which also uses the complete current list rather than this optimization. Successful compaction starts a new prefix with the tools currently provided; resuming in a new process does not replay previous tool changes. A stable request shape does not guarantee cached tokens or lower cost. + ## What's next? Now that you've got the basics, here are more powerful features to explore: diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index b3932ce66a..2078b0cfe5 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -112,6 +112,53 @@ let client = Client::start(ClientOptions::new() The OTLP protocol field configures the CLI's `"otlp-http"` exporter for all signals. Leave it unset to use the CLI default, or set it to `"http/protobuf"` to export protobuf over HTTP. +### Skill and command telemetry + +The runtime follows the [GenAI skill semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/commit/771e3210f7518694666834f7243573b8018c7a4f) +by refining existing `execute_tool` spans, not adding a separate skill +operation. A loader is named `execute_tool skill {skill}`. An unambiguously +identified resource or script is named +`execute_tool {tool} {skill} {resource}`, with a skill-relative resource name. +Other command spans append the actual known launcher name. + +* `gen_ai.skill.name` and `gen_ai.skill.resource.name`: Available independently of content capture +* `gen_ai.skill.description` and `gen_ai.skill.source.uri`: Require content capture through `TelemetryConfig` +* `process.executable.name`: The directly launched executable, such as the shell launcher, not a guessed command token +* `process.executable.path`: An absolute known path; requires content capture +* `process.exit.code`: An integer, including zero, when a command-executing tool reports an exit; unavailable exits and pre-spawn failures are omitted + +Poll tools do not inherit process attributes from the command they read. +Sandboxed executable identities are omitted when the sandbox backend does +not expose them. A nonzero exit does not imply that the tool itself failed. +Skill definitions and directory listings are not named skill resources. +Preloaded skills produce invocation events without a fabricated tool span. +Repeated preparation of the same selected-agent skill context emits one receipt; +changed content or provenance emits another. Child sessions report their own +preloads independently. Detached command spans identify the directly spawned +Unix `sh` supervisor or selected Windows PowerShell executable. + +Custom tools that override built-in names do not inherit native skill or process +attributes. Visible, enabled skills still own resources when model invocation +is disabled. Script attribution uses the post-hook command and the selected +shell's directory; it is omitted when startup scripts or profiles make the +execution context uncertain. + +While managed telemetry settings are unresolved, pending tracking buffers only +capture-independent tool facts. Descriptions, source URIs, and executable paths +observed before the policy resolves are omitted, even if capture is later enabled. +The final policy governs replay; disabled or cancelled tracking discards the buffer. + +These are breaking telemetry-name changes: use `gen_ai.skill.name` instead +of `github.copilot.tool.parameters.skill_name` or +`github.copilot.skill.name`, and use `gen_ai.skill.source.uri` instead of +`github.copilot.skill.path`. Local source values are file URIs, not bare paths. +The `github.copilot.skill.invoked` event and its content, source-category, +trigger, and plugin fields remain Copilot-specific. Skill content retains its +existing capture gate. + +The skill attributes and span refinements have Development stability; the +process attributes have Release Candidate stability. + ### Trace context propagation > **Most users don't need this.** The `TelemetryConfig` above is all you need to collect traces from the CLI. The trace context propagation described in this section is an **advanced feature** for applications that create their own OpenTelemetry spans and want them to appear in the **same distributed trace** as the CLI's spans. diff --git a/docs/troubleshooting/compatibility.md b/docs/troubleshooting/compatibility.md index da8bf0daae..6054316f50 100644 --- a/docs/troubleshooting/compatibility.md +++ b/docs/troubleshooting/compatibility.md @@ -33,6 +33,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Abort | `abort()` | Cancel in-flight request | | **Tools** | | | | Register custom tools | `registerTools()` | Full JSON Schema support | +| Replace custom tools (mid-session) | `session.setTools()` | Experimental; see [Changing tools during a session](../features/changing-tools.md) | | Tool permission control | `onPreToolUse` hook | Allow/deny/ask | | Tool result modification | `onPostToolUse` hook | Transform results | | Available/excluded tools | `availableTools`, `excludedTools` config | Filter tools | diff --git a/dotnet/README.md b/dotnet/README.md index 654fc50816..1269c6c057 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -110,6 +110,7 @@ new CopilotClient(CopilotClientOptions? options = null) - `GitHubToken` - GitHub token for authentication. When provided, takes priority over other auth methods. - `UseLoggedInUser` - Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `RuntimeConnection.ForUri(...)`. - `Telemetry` - OpenTelemetry configuration for the runtime process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `SessionFs` - Custom session filesystem provider configuration. To serve provider-only images to the `view` tool, set `Capabilities.Binary` to `true` and return a provider implementing `ISessionFsBinaryProvider` (`ReadFileBytesAsync` and `WriteFileBytesAsync`) from `SessionConfig.CreateSessionFsProvider`. Image reads do not fall back to local files. Binary reads and writes are limited to 50,330,880 raw bytes (approximately 48 MiB); larger results return a filesystem error before encoding or decoding. - `InstallationConfirmationHandler` - Experimental connection-global human review for `installations.confirm`. Receives the typed request and one cancellation token that is cancelled when the request is retired or the connection closes, and returns an explicit decision. Does not enable installation capabilities. #### RuntimeConnection @@ -206,6 +207,7 @@ Create a new conversation session. - `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory. - `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled. - `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`. +- `SkillProvider` - Experimental session-scoped skill provider. See [Skill providers (experimental)](#skill-providers-experimental). - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`. @@ -224,7 +226,18 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. +- `SkillProvider` - Re-supplies the session-scoped skill provider when resuming. - `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. +- `AllowTranscriptRecovery` - Repairs a damaged transcript when true. The default + is true in all modes; set false to reject recovery. `session.TranscriptRecovery` contains + `PlannedBackupPath`, `InvalidLineNumbers` (including torn-tail loss), and + `SessionStartMoved` when repair is reported; otherwise it is null. On rejection, + `ResumeSessionAsync` throws an `IOException` whose `InnerException` is a + `RemoteRpcException`. Read `ErrorCode` (`-32075`) and `ErrorData` from that inner + exception for the server's typed error and its `invalidLineNumbers` / + `sessionStartMoved` fields. The outer message retains the existing communication-error + prefix. Disabling recovery still permits adding a missing newline after an intact + final record; it rejects torn tails. ```csharp await using var session = await client.CreateSessionAsync(new SessionConfig @@ -243,6 +256,62 @@ await using var session = await client.CreateSessionAsync(new SessionConfig Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +##### Skill providers (experimental) + +Set `SessionConfig.SkillProvider` or `ResumeSessionConfig.SkillProvider` to +serve session-scoped skills from your application. The provider is not persisted: +pass it again on every resume, because resuming without one unbinds it. Skill +providers are only supported for local sessions; `CreateSessionAsync` throws if +`Cloud` and `SkillProvider` are both set. The skill provider types are +experimental and raise the `GHCP001` diagnostic; suppress it with +`#pragma warning disable GHCP001` or `GHCP001`. + +```csharp +#pragma warning disable GHCP001 // Skill providers are experimental. + +public sealed class MySkillProvider : ISkillProvider +{ + public Task> ListSkillsAsync(CancellationToken cancellationToken) => + Task.FromResult>( + [ + new() + { + Name = "project-facts", + Description = "Important facts about this host application", + ArgumentHint = "" + } + ]); + + public Task ReadSkillAsync(string name, CancellationToken cancellationToken) => + Task.FromResult(name == "project-facts" + ? """ + # Project facts + + Use the application's project index before answering. + """ + : null); +} + +await using var client = new CopilotClient(new CopilotClientOptions +{ + Mode = CopilotClientMode.Empty +}); + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + AvailableTools = [], // Empty mode requires an explicit tool allow-list. + SkillProvider = new MySkillProvider(), + EnableSkills = true // Required in empty mode; otherwise skills default off. +}); +``` + +The runtime may call `ListSkillsAsync` and `ReadSkillAsync` concurrently. Honor +the supplied cancellation token; it is canceled when the runtime abandons the +request (for example, on timeout, session disposal, or a resume that replaces the +provider) or the connection closes. Return `null` from `ReadSkillAsync` when a +skill name is not found. Optional `SkillProviderDescriptor` properties are +omitted from JSON when unset. + ##### `PingAsync(string? message = null): Task` Ping the server to check connectivity. @@ -347,6 +416,19 @@ await session.SendAndWaitAsync(new MessageOptions Agent sources serialize as `agent-`. Pass the agent ID without adding a prefix. The SDK preserves its case and whitespace and rejects null IDs. +##### `SetToolsAsync(ICollection tools, CancellationToken cancellationToken = default): Task` (experimental) + +Replace the complete set of externally implemented tools supplied by this client +on a live session. Pass the same tool declarations used in `SessionConfig.Tools` +or `ResumeSessionConfig.Tools`; an empty collection removes this client's tools. +Built-in, MCP/plugin, extension, subagent, and other clients' tools are unchanged. + +Handlers switch after the runtime accepts the replacement. Running tool calls +finish on the handlers that started them, rejected replacements leave the +previous handlers installed, and concurrent replacements are applied in order. +See [Changing tools](../docs/features/changing-tools.md) for shared behavior and +active-turn limitations. + ##### Structured outputs (experimental) Use `SendAndWaitAsync` to infer a JSON Schema from a .NET type and @@ -495,6 +577,13 @@ Abort the currently processing message in this session. Get all events/messages from this session. +Live notifications and history deserialization select source-generated metadata for the +received event type rather than initializing every event type on the first event. +History parsing uses a bounded stack or pooled UTF-8 buffer to avoid an extra +reader-scoping pass; common event-type selection does not allocate a discriminator string. +Unknown or missing event types retain the base `SessionEvent` fallback; serialization +and malformed-event validation are unchanged. + ##### `DisposeAsync(): ValueTask` Close the session and release in-memory resources. Session data on disk is preserved — the conversation can be resumed later via `ResumeSessionAsync()`. To permanently delete session data, use `client.DeleteSessionAsync()`. @@ -787,6 +876,16 @@ var session = await client.CreateSessionAsync(new SessionConfig }); ``` +An explicit `apply_patch` override may advertise a root string schema instead of +an object schema. The runtime delivers the patch as a string in +`ToolInvocation.Arguments`; the SDK binds that scalar to the named `input` +argument required by `AIFunction`. A schema wrapper around a typed handler should +therefore use a parameter named `input`. Object-schema tools retain their +declared parameter names. + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `CopilotToolOptions.SkipPermission` to allow a tool to execute without triggering a permission prompt: @@ -965,6 +1064,8 @@ var session = await client.CreateSessionAsync(new SessionConfig Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Preamble`, `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`. `Identity` and `ToolInstructions` are section groups that target a collection of related sub-sections as a unit; use `Preamble` to target just the identity preamble. +`SystemMessageSection.LastInstructions` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a `SectionOverride.Transform` callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SystemMessageSection.RuntimeInstructions` is a separate section: removing it does not remove `SystemMessageSection.LastInstructions`. + Each section override supports five actions: `Replace`, `Remove`, `Append`, `Prepend`, and `Preserve` (a no-op that opts an individually-addressable section out of a group-level `Remove`). Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored. #### Replace Mode @@ -1253,6 +1354,32 @@ var session = await client.CreateSessionAsync(new SessionConfig - `OnSessionStart` - Run logic when a session starts or resumes. - `OnSessionEnd` - Cleanup or logging when session ends. - `OnErrorOccurred` - Handle errors with retry/skip/abort strategies. +- `OnAgentStop` - Intercept a natural stop of the top-level agent. +- `OnSubagentStart` - Add context to a sub-agent before its first turn. +- `OnSubagentStop` - Inspect a sub-agent's last response, block the stop with a follow-up instruction, or replace its response. + +Sub-agent lifecycle hooks are registered on the parent session. Their input includes the parent `SessionId`, a `DateTimeOffset` timestamp, `WorkingDirectory`, `TranscriptPath` (empty if unavailable), and `AgentName`; `AgentDisplayName` and `AgentDescription` are optional. `OnSubagentStop` also receives an optional `AgentId`, `AgentType`, `StopReason` (currently `"end_turn"`), and the sub-agent's `Response`. `HookInvocation.SessionId` is the parent session ID. + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnSubagentStart = (input, invocation) => + Task.FromResult(new SubagentStartHookOutput + { + AdditionalContext = "Read the requested file before answering." + }), + OnSubagentStop = (input, invocation) => + Task.FromResult(new SubagentStopHookOutput + { + ModifiedResponse = input.Response.Trim() + }) + } +}); +``` + +`OnSubagentStart`'s `AdditionalContext` is prepended to the child's initial prompt. To request another child turn instead of accepting its response, return `new SubagentStopHookOutput { Decision = "block", Reason = "Explain your findings." }` from `OnSubagentStop`; a non-empty `Reason` is required. Otherwise, `ModifiedResponse` replaces the response reported to the parent. These hooks are distinct from `OnAgentStop`, which applies to the top-level agent. ## Elicitation Requests diff --git a/dotnet/src/Canvas.cs b/dotnet/src/Canvas.cs index 6bf8be984e..02874a79f2 100644 --- a/dotnet/src/Canvas.cs +++ b/dotnet/src/Canvas.cs @@ -171,6 +171,12 @@ public interface ICanvasHandler /// Handle a non-lifecycle action declared by the canvas. /// Default: throws . /// + /// + /// The returned value is sent to the model as the invoke_canvas_action + /// tool result. To return text and images, as tool handlers do, return a + /// with ; + /// any other value is rendered to the model as JSON text. + /// Task OnActionAsync(CanvasProviderInvokeActionRequest context, CancellationToken cancellationToken); } diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f43b32234c..ab4399ef5c 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -892,6 +892,7 @@ private CopilotSession InitializeSession( session.RegisterElicitationHandler(config.OnElicitationRequest); session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest); session.RegisterAutoModeSwitchHandler(config.OnAutoModeSwitchRequest); + session.RegisterSkillProvider(config.SkillProvider); if (config.OnUserInputRequest != null) { session.RegisterUserInputHandler(config.OnUserInputRequest); @@ -1190,6 +1191,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { ArgumentNullException.ThrowIfNull(config); ValidateGitHubTokenConfig(config); + if (config.Cloud is not null && config.SkillProvider is not null) + { + throw new ArgumentException("Skill providers are not supported for cloud sessions."); + } var connection = await EnsureConnectedAsync(cancellationToken); var totalTimestamp = Stopwatch.GetTimestamp(); @@ -1208,7 +1213,9 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Hooks.OnSessionStart != null || config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null || - config.Hooks.OnAgentStop != null); + config.Hooks.OnAgentStop != null || + config.Hooks.OnSubagentStart != null || + config.Hooks.OnSubagentStop != null); var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); @@ -1227,6 +1234,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance var registrationId = RegisterGitHubTokenProvider(config.GitHubTokenProvider); var registrationTransferred = false; CopilotSession? session = null; + string? serverAssignedSessionId = null; try { if (localSessionId != null) @@ -1324,7 +1332,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance GitHubMcpToolConfig: config.GitHubMcpToolConfig, ManagedSettings: config.ManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, - AdditionalDirectories: config.AdditionalDirectories); + AdditionalDirectories: config.AdditionalDirectories, + HasSkillProvider: config.SkillProvider is not null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); @@ -1343,6 +1352,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance && sessionIdProp.GetString() is string sessionId && !string.IsNullOrEmpty(sessionId)) { + serverAssignedSessionId = sessionId; session = InitializeSession( sessionId, connection.Rpc, @@ -1350,6 +1360,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance transformCallbacks, hasHooks, "CopilotClient.CreateSessionAsync"); + serverAssignedSessionId = null; } }; @@ -1391,6 +1402,22 @@ public async Task CreateSessionAsync(SessionConfig config, Cance catch (Exception ex) { session?.Unregister(); + if (localSessionId is null && serverAssignedSessionId is not null) + { + try + { + using var cleanupTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + await DeleteSessionAsync(serverAssignedSessionId, cleanupTimeout.Token).ConfigureAwait(false); + } + catch (Exception cleanupError) when (cleanupError is OperationCanceledException + or IOException + or SocketException + or InvalidOperationException + or ObjectDisposedException) + { + _logger.LogWarning(cleanupError, "Failed to delete cloud session {SessionId} after creation failed", serverAssignedSessionId); + } + } if (ex is not OperationCanceledException) { @@ -1465,7 +1492,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Hooks.OnSessionStart != null || config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null || - config.Hooks.OnAgentStop != null); + config.Hooks.OnAgentStop != null || + config.Hooks.OnSubagentStart != null || + config.Hooks.OnSubagentStop != null); var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); @@ -1569,7 +1598,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes GitHubMcpToolConfig: config.GitHubMcpToolConfig, ManagedSettings: config.ManagedSettings, EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, - AdditionalDirectories: config.AdditionalDirectories); + AdditionalDirectories: config.AdditionalDirectories, + AllowTranscriptRecovery: config.AllowTranscriptRecovery, + HasSkillProvider: config.SkillProvider is not null ? true : null); var rpcTimestamp = Stopwatch.GetTimestamp(); var response = await InvokeRpcAsync( @@ -1580,6 +1611,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes sessionId); session.WorkspacePath = response.WorkspacePath; + session.TranscriptRecovery = response.TranscriptRecovery; session.SetCapabilities(response.Capabilities); session.SetOpenCanvases(response.OpenCanvases); @@ -1782,6 +1814,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell if (_sessions.TryRemove(sessionId, out var session)) { + session.ClearSkillProvider(); session.ReleaseGitHubTokenProviderRegistration(); } } @@ -2194,6 +2227,11 @@ private void ConfigureSessionFsHandlers(CopilotSession session, Func ConnectToServerAsync(Process? cliProcess, string? rpc.SetLocalRpcMethod("autoModeSwitch.request", handler.OnAutoModeSwitchRequest); rpc.SetLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke); rpc.SetLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform); + rpc.SetLocalRpcMethod("skillProvider.list", handler.OnSkillProviderList, singleObjectParam: true); + rpc.SetLocalRpcMethod("skillProvider.read", handler.OnSkillProviderRead, singleObjectParam: true); ClientSessionApiRegistration.RegisterClientSessionApiHandlers(rpc, sessionId => { var session = GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); @@ -2834,6 +2874,7 @@ private void CancelPendingExternalTools() } foreach (var session in _sessions.Values) { + session.ClearSkillProvider(); session.CancelPendingExternalTools(); } } @@ -2862,6 +2903,8 @@ private static JsonSerializerOptions CreateSerializerOptions() DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + options.Converters.Add(SessionEventJsonConverter.Default); + options.TypeInfoResolverChain.Add(SessionEventJsonTypeInfoResolver.Default); options.TypeInfoResolverChain.Add(ClientJsonContext.Default); options.TypeInfoResolverChain.Add(TypesJsonContext.Default); options.TypeInfoResolverChain.Add(CopilotSession.SessionJsonContext.Default); @@ -3007,6 +3050,32 @@ public async ValueTask OnSystemMessageTransfo return await session.HandleSystemMessageTransformAsync(sections); } + public async ValueTask OnSkillProviderList(SkillProviderListRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (string.IsNullOrEmpty(request.SessionId)) + { + throw new ArgumentException("Skill provider list request is missing a session id."); + } + + var session = client.GetSession(request.SessionId) + ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}"); + return await session.HandleSkillProviderListAsync(cancellationToken); + } + + public async ValueTask OnSkillProviderRead(SkillProviderReadRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (string.IsNullOrEmpty(request.SessionId) || string.IsNullOrEmpty(request.Name)) + { + throw new ArgumentException("Skill provider read request is missing a session id or skill name."); + } + + var session = client.GetSession(request.SessionId) + ?? throw new InvalidOperationException($"No skill provider for session: {request.SessionId}"); + return await session.HandleSkillProviderReadAsync(request.Name, cancellationToken); + } + } private class Connection( @@ -3165,7 +3234,8 @@ internal record CreateSessionRequest( [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null, [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null, - IList? AdditionalDirectories = null); + IList? AdditionalDirectories = null, + bool? HasSkillProvider = null); #pragma warning restore GHCP001 internal record ToolDefinition( @@ -3286,7 +3356,9 @@ internal record ResumeSessionRequest( [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null, [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null, - IList? AdditionalDirectories = null); + IList? AdditionalDirectories = null, + bool? AllowTranscriptRecovery = null, + bool? HasSkillProvider = null); #pragma warning restore GHCP001 internal record ResumeSessionResponse( @@ -3294,7 +3366,8 @@ internal record ResumeSessionResponse( string? WorkspacePath, SessionCapabilities? Capabilities = null, #pragma warning disable GHCP001 - IList? OpenCanvases = null); + IList? OpenCanvases = null, + TranscriptRecoveryReport? TranscriptRecovery = null); #pragma warning restore GHCP001 internal record CommandWireDefinition( @@ -3373,6 +3446,21 @@ internal record UserInputRequestResponse( string Answer, bool WasFreeform); + internal record SkillProviderListRequest( + string SessionId); + +#pragma warning disable GHCP001 + internal record SkillProviderListResult( + IReadOnlyList Skills); +#pragma warning restore GHCP001 + + internal record SkillProviderReadRequest( + string SessionId, + string Name); + + internal record SkillProviderReadResult( + [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? Markdown); + internal record AutoModeSwitchRequestResponse( AutoModeSwitchResponse Response); @@ -3401,6 +3489,12 @@ internal record HooksInvokeResponse( [JsonSerializable(typeof(ExitPlanModeResult))] [JsonSerializable(typeof(GetLastSessionIdResponse))] [JsonSerializable(typeof(HooksInvokeResponse))] + [JsonSerializable(typeof(SkillProviderListRequest))] +#pragma warning disable GHCP001 + [JsonSerializable(typeof(SkillProviderListResult))] +#pragma warning restore GHCP001 + [JsonSerializable(typeof(SkillProviderReadRequest))] + [JsonSerializable(typeof(SkillProviderReadResult))] [JsonSerializable(typeof(ListSessionsRequest))] [JsonSerializable(typeof(ListSessionsResponse))] [JsonSerializable(typeof(GetSessionMetadataRequest))] diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 3f97448751..eb06ae1b62 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -576,6 +576,8 @@ public sealed class ModelBillingTokenPricesLongContext [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonPropertyName("cachePrice")] public double? CachePrice { get; set; } @@ -596,6 +598,8 @@ public sealed class ModelBillingTokenPricesLongContext [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonPropertyName("contextMax")] public long? ContextMax { get; set; } @@ -625,6 +629,8 @@ public sealed class ModelBillingTokenPrices [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonPropertyName("cachePrice")] public double? CachePrice { get; set; } @@ -645,6 +651,8 @@ public sealed class ModelBillingTokenPrices [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonPropertyName("contextMax")] public long? ContextMax { get; set; } @@ -737,6 +745,10 @@ public sealed class ModelCapabilitiesSupports [JsonPropertyName("reasoningEffort")] public bool? ReasoningEffort { get; set; } + /// Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. + [JsonPropertyName("thinking")] + public bool? Thinking { get; set; } + /// Whether this model supports canonical tool calling. [JsonPropertyName("toolCalls")] public bool? ToolCalls { get; set; } @@ -867,6 +879,10 @@ public sealed class Model [JsonPropertyName("supportedReasoningEfforts")] public IList? SupportedReasoningEfforts { get; set; } + /// Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. + [JsonPropertyName("vendor")] + public string? Vendor { get; set; } + /// Warnings the service published for this model, such as a deprecated client version. Present only when the service published at least one warning. The model remains usable; hosts should surface these as advisory rather than blocking. [JsonPropertyName("warningMessages")] public IList? WarningMessages { get; set; } @@ -950,6 +966,234 @@ public sealed class SandboxHostSupport public bool Supported { get; set; } } +/// Status of the persistent certificate authority of the sandbox credential proxy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxProxyCaStatus +{ + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + [JsonPropertyName("canInstall")] + public bool CanInstall { get; set; } + + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// The state of the certificate authority. + [JsonPropertyName("state")] + public SandboxProxyCaState State { get; set; } +} + +/// Credential-injection capability flags applied while the sandbox is enabled. For the same capability independent of sandboxing, and matched to the credential's GitHub host, see `shell.credentials`; the two are additive. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigAuth +{ + /// Whether to authenticate sandboxed gh through the local masking proxy. The child receives a fake GH_TOKEN; its real value is substituted only at github.com, api.github.com and uploads.github.com (github.com because gh repo clone authenticates git through gh auth git-credential). The repository's GitHub account takes precedence over the Copilot login. Default: false (opt-in). + [JsonPropertyName("gh")] + public bool? Gh { get; set; } + + /// Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http.<url>.extraheader`; the real Authorization header is substituted only at its original HTTPS host, port, and repository path scope. github.com uses the Copilot token; other forges use credentials resolved from the user's own helper on the host. Default: false (opt-in). + [JsonPropertyName("git")] + public bool? Git { get; set; } +} + +/// Destinations authorized to receive one masked environment credential. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxMaskedEnvVar +{ + /// Nonempty list of HTTPS injection hostnames or *.example.com patterns. Bare * is not accepted. These grants never override the sandbox network policy. Values in plaintext HTTP requests, URLs, bodies, encoded credentials, and signed requests are not substituted. + [JsonPropertyName("injectHosts")] + public IList InjectHosts { get => field ??= []; set; } +} + +/// Whole-value environment credential masking for sandboxed children. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxCredentialsConfig +{ + /// Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. + [JsonPropertyName("envVars")] + public IDictionary EnvVars { get => field ??= new Dictionary(); set; } +} + +/// macOS seatbelt experimental options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyExperimentalSeatbelt +{ + /// Whether the macOS seatbelt profile may access the keychain. + [JsonPropertyName("keychainAccess")] + public bool? KeychainAccess { get; set; } +} + +/// Platform-specific experimental policy fields. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyExperimental +{ + /// macOS seatbelt experimental options. + [JsonPropertyName("seatbelt")] + public SandboxConfigUserPolicyExperimentalSeatbelt? Seatbelt { get; set; } +} + +/// Filesystem rules to merge into the base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyFilesystem +{ + /// Whether to clear the policy when the session exits. + [JsonPropertyName("clearPolicyOnExit")] + public bool? ClearPolicyOnExit { get; set; } + + /// Paths explicitly denied. + [JsonPropertyName("deniedPaths")] + public IList? DeniedPaths { get; set; } + + /// Paths granted read-only access. + [JsonPropertyName("readonlyPaths")] + public IList? ReadonlyPaths { get; set; } + + /// Paths granted read/write access. + [JsonPropertyName("readwritePaths")] + public IList? ReadwritePaths { get; set; } +} + +/// HTTP proxy configuration for sandboxed traffic. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyNetworkProxy +{ + /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. + [JsonPropertyName("password")] + public string? Password { get; set; } + + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. + [JsonPropertyName("username")] + public string? Username { get; set; } +} + +/// Network rules to merge into the base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicyNetwork +{ + /// Hosts allowed through the built-in sandbox proxy. A non-empty list denies unmatched hosts; an absent or empty list allows all hosts not blocked. Supports exact hostnames, IP addresses, and *.example.com for strict subdomains. Host rules do not override the outbound or local-network toggles. + [JsonPropertyName("allowedHosts")] + public IList? AllowedHosts { get; set; } + + /// Whether traffic to local/loopback addresses is allowed. + [JsonPropertyName("allowLocalNetwork")] + public bool? AllowLocalNetwork { get; set; } + + /// Whether outbound network traffic is allowed at all. + [JsonPropertyName("allowOutbound")] + public bool? AllowOutbound { get; set; } + + /// Hosts denied by the built-in sandbox proxy. Deny rules take precedence over allowedHosts. A domain also denies all its subdomains. IP addresses match exactly; *.example.com matches strict subdomains, and * denies every host. + [JsonPropertyName("blockedHosts")] + public IList? BlockedHosts { get; set; } + + /// HTTP(S) proxy for sandboxed traffic. This is the built-in local proxy's upstream: every sandboxed command reaches it through a loopback listener, so credentials stay in the runtime and never reach the child. On Windows the sandbox also needs local network access, because it reaches that listener over host loopback. Configure credentials in the separate username/password fields. The transient local listener URL is never persisted. + [JsonPropertyName("proxy")] + public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } +} + +/// macOS seatbelt-specific options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicySeatbelt +{ + /// Whether the macOS seatbelt profile may access the keychain. + [JsonPropertyName("keychainAccess")] + public bool? KeychainAccess { get; set; } +} + +/// User-managed sandbox policy fragment merged into the auto-discovered base policy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfigUserPolicy +{ + /// Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. + [JsonPropertyName("experimental")] + public SandboxConfigUserPolicyExperimental? Experimental { get; set; } + + /// Filesystem rules to merge into the base policy. + [JsonPropertyName("filesystem")] + public SandboxConfigUserPolicyFilesystem? Filesystem { get; set; } + + /// Network rules to merge into the base policy. + [JsonPropertyName("network")] + public SandboxConfigUserPolicyNetwork? Network { get; set; } + + /// macOS seatbelt options to merge into the base policy. + [JsonPropertyName("seatbelt")] + public SandboxConfigUserPolicySeatbelt? Seatbelt { get; set; } +} + +/// Resolved sandbox configuration. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxConfig +{ + /// Whether to auto-add the current working directory to readwritePaths. Default: true. + [JsonPropertyName("addCurrentWorkingDirectory")] + public bool? AddCurrentWorkingDirectory { get; set; } + + /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + [JsonPropertyName("allowBypass")] + public bool? AllowBypass { get; set; } + + /// 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; } + + /// Credential-injection capability flags. + [JsonPropertyName("auth")] + public SandboxConfigAuth? Auth { get; set; } + + /// Opt-in whole-value environment masking for sandboxed shell, MCP, and LSP children. Configured names get random sentinels; the local proxy substitutes them only in HTTPS request headers at their injection hosts. Approved bypasses skip masking and the sandbox proxy, so bypassed shells may receive the real environment values. Disabled or explicitly opted-out routes are not protected. No credential values are stored in this configuration. + [JsonPropertyName("credentials")] + public SandboxCredentialsConfig? Credentials { get; set; } + + /// Whether sandboxing is enabled for the session. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + [JsonInclude] + [JsonPropertyName("managedLspRoutingLocked")] + internal bool? ManagedLspRoutingLocked { get; set; } + + /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + [JsonInclude] + [JsonPropertyName("managedMcpRoutingLocked")] + internal bool? ManagedMcpRoutingLocked { get; set; } + + /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxLspServers")] + public bool? SandboxLspServers { get; set; } + + /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxMcpServers")] + public bool? SandboxMcpServers { get; set; } + + /// User-managed sandbox policy fragment merged into the auto-discovered base policy. + [JsonPropertyName("userPolicy")] + public SandboxConfigUserPolicy? UserPolicy { get; set; } +} + +/// Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SandboxProxyCaRequest +{ + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + [JsonPropertyName("sandboxConfig")] + public SandboxConfig? SandboxConfig { get; set; } +} + +/// Result of creating the persistent certificate authority of the sandbox credential proxy. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SandboxProxyCaCreateResult +{ + /// Absolute path of the public certificate of the certificate authority, in PEM format. + [JsonPropertyName("certificatePath")] + public string CertificatePath { get; set; } = string.Empty; +} + /// Built-in tool metadata with identifier, optional namespaced name, description, input-parameter schema, and usage instructions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class Tool @@ -1069,6 +1313,7 @@ internal sealed class AccountGetQuotaRequest [JsonDerivedType(typeof(AuthInfoTokenProvider), "token-provider")] [JsonDerivedType(typeof(AuthInfoCopilotApiToken), "copilot-api-token")] [JsonDerivedType(typeof(AuthInfoUser), "user")] +[JsonDerivedType(typeof(AuthInfoAccount), "account")] [JsonDerivedType(typeof(AuthInfoGhCli), "gh-cli")] [JsonDerivedType(typeof(AuthInfoApiKey), "api-key")] public partial class AuthInfo @@ -1565,6 +1810,24 @@ public partial class AuthInfoUser : AuthInfo public required string Login { get; set; } } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthInfoAccount : AuthInfo +{ + /// + [JsonIgnore] + public override string Type => "account"; + + /// Host coordinate owned by the account's model provider. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// Login identifying the provider-owned account. + [JsonPropertyName("login")] + public required string Login { get; set; } +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// The gh-cli variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -6514,6 +6777,128 @@ internal sealed class AgentsGetDiscoveryPathsRequest public IList? ProjectPaths { get; set; } } +/// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinsResult +{ + /// The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. + [JsonPropertyName("disableableNames")] + public IList DisableableNames { get => field ??= []; set; } + + /// Every agent name this runtime ships. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } + + /// The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. + [JsonPropertyName("yamlBasedNames")] + public IList YamlBasedNames { get => field ??= []; set; } +} + +/// A shipped agent, named and described. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class BuiltinAgentSummary +{ + /// One-line description of what the agent does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// The agent name, as it appears in `getBuiltins`. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// The shipped agents available under the requested flags. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetAvailableBuiltinsResult +{ + /// Available shipped agents, in the runtime's own order. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } +} + +/// The feature flags to evaluate shipped agents against. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetAvailableBuiltinsRequest +{ + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + [JsonPropertyName("context")] + public string? Context { get; set; } + + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + [JsonPropertyName("featureFlags")] + public IDictionary? FeatureFlags { get; set; } + + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + [JsonPropertyName("overrides")] + public IDictionary? Overrides { get; set; } +} + +/// One shipped agent's definition. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinDefinitionResult +{ + /// The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. + [JsonPropertyName("definitionJson")] + public string DefinitionJson { get; set; } = string.Empty; +} + +/// The shipped agent whose definition to load. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinDefinitionRequest +{ + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// One shipped agent, projected for a listing. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinListingDefinitionResult +{ + /// The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. + [JsonPropertyName("definitionJson")] + public string DefinitionJson { get; set; } = string.Empty; +} + +/// The shipped agent whose listing entry to load. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsGetBuiltinListingDefinitionRequest +{ + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// The model to switch to, and the warning to show when the agent's preference could not be met. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsCustomAgentInitialModelDecisionResult +{ + /// The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + + /// The first available model that matches the agent's preferences. Absent when none of the requested models is available. + [JsonPropertyName("targetModel")] + public string? TargetModel { get; set; } + + /// What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. + [JsonPropertyName("warning")] + public string? Warning { get; set; } +} + +/// The models a custom agent asks for, and the models actually available. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AgentsCustomAgentInitialModelDecisionParams +{ + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + [JsonPropertyName("agentModelsJson")] + public string AgentModelsJson { get; set; } = string.Empty; + + /// The models available to this session, serialized in the shape the model list carries. + [JsonPropertyName("availableModelsJson")] + public string AvailableModelsJson { get; set; } = string.Empty; +} + /// Loaded instruction source for a session, including path, content, category, location, applicability, and optional description. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class InstructionSource @@ -6628,6 +7013,179 @@ internal sealed class InstructionsGetDiscoveryPathsRequest public IList? ProjectPaths { get; set; } } +/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class InstalledPlugin +{ + /// Path where the plugin is cached locally. + [JsonPropertyName("cache_path")] + public string? CachePath { get; set; } + + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Installation timestamp. + [JsonPropertyName("installed_at")] + public string InstalledAt { get; set; } = string.Empty; + + /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. + [JsonPropertyName("installed_from")] + public string? InstalledFrom { get; set; } + + /// Marketplace the plugin came from (empty string for direct repo installs). + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; + + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Source for direct repo installs (when marketplace is empty). + [JsonPropertyName("source")] + public JsonElement? Source { get; set; } + + /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. + [JsonPropertyName("source_sha")] + public string? SourceSha { get; set; } + + /// Version installed (if available). + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class LoggedInUser +{ + /// Source account this account was derived from, when one was recorded. + [JsonPropertyName("derivedFrom")] + public string? DerivedFrom { get; set; } + + /// Host the account belongs to, such as `github.com` or an Enterprise server. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Account kind, when the host recorded one. Consumers must tolerate new strings. + [JsonPropertyName("kind")] + public string? Kind { get; set; } + + /// Account login on that host. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; +} + +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GlobalStateLoadResult +{ + /// Whether the user has answered the prompt suggesting they install the desktop app. + [JsonPropertyName("appInstallNudgeResponded")] + public bool? AppInstallNudgeResponded { get; set; } + + /// Whether the app tip has been shown. + [JsonPropertyName("appTipShown")] + public bool? AppTipShown { get; set; } + + /// Terminals the user has already been asked to set up, so the host does not ask twice. + [JsonPropertyName("askedSetupTerminals")] + public IList? AskedSetupTerminals { get; set; } + + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + [JsonPropertyName("autoFeedbackLastPromptedAt")] + public string? AutoFeedbackLastPromptedAt { get; set; } + + /// When the host first ran on this machine. + [JsonPropertyName("firstLaunchAt")] + public string? FirstLaunchAt { get; set; } + + /// Plugins installed on this machine. + [JsonPropertyName("installedPlugins")] + public IList? InstalledPlugins { get; set; } + + /// Account used for the most recent sign-in. + [JsonPropertyName("lastLoggedInUser")] + public LoggedInUser? LastLoggedInUser { get; set; } + + /// Every account the host has signed in to on this machine. + [JsonPropertyName("loggedInUsers")] + public IList? LoggedInUsers { get; set; } + + /// Whether the one-off cleanup of stored reasoning summaries has run. + [JsonPropertyName("reasoningSummariesCleanupDone")] + public bool? ReasoningSummariesCleanupDone { get; set; } + + /// Models the user selected recently, most recent first. + [JsonPropertyName("recentModelIds")] + public IList? RecentModelIds { get; set; } + + /// Whether the user declined to trust the sandbox credential proxy CA. + [JsonPropertyName("sandboxCredentialProxyCaDeclined")] + public bool? SandboxCredentialProxyCaDeclined { get; set; } + + /// Whether the sandbox onboarding has been shown. + [JsonPropertyName("sandboxOnboardingShown")] + public bool? SandboxOnboardingShown { get; set; } + + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + [JsonPropertyName("staff")] + public bool? Staff { get; set; } + + /// Whether the user was recognized as GitHub staff. + [JsonPropertyName("staffGithub")] + public bool? StaffGitHub { get; set; } + + /// When the staff-only log level migration last ran. + [JsonPropertyName("staffLogLevelMigrationAt")] + public string? StaffLogLevelMigrationAt { get; set; } + + /// Whether the user was recognized as Microsoft staff. + [JsonPropertyName("staffMicrosoft")] + public bool? StaffMicrosoft { get; set; } + + /// When the staff-only model reset last ran. + [JsonPropertyName("staffModelResetAt")] + public string? StaffModelResetAt { get; set; } + + /// When the staff-only update channel migration last ran. + [JsonPropertyName("staffUpdateChannelMigrationAt")] + public string? StaffUpdateChannelMigrationAt { get; set; } + + /// Folders where the user declined the init prompt, so it stays hidden there. + [JsonPropertyName("suppressInitFolders")] + public IList? SuppressInitFolders { get; set; } + + /// Folders the user has marked as trusted. + [JsonPropertyName("trustedFolders")] + public IList? TrustedFolders { get; set; } +} + +/// Selects the configuration directory whose machine-wide state to read. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GlobalStateLoadForConfigDirRequest +{ + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } +} + +/// A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GlobalStateWriteKeyRequest +{ + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } + + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + [JsonPropertyName("value")] + public JsonElement? Value { get; set; } +} + /// A literal choice the command input accepts, with a human-facing description. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SlashCommandInputChoice @@ -6729,7 +7287,7 @@ public sealed class UserSettingMetadata public JsonElement Value { get; set; } } -/// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. +/// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class UserSettingsGetResult { @@ -6738,15 +7296,6 @@ public sealed class UserSettingsGetResult public IDictionary Settings { get => field ??= new Dictionary(); set; } } -/// Outcome of writing user settings. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserSettingsSetResult -{ - /// Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. - [JsonPropertyName("shadowedKeys")] - public IList ShadowedKeys { get => field ??= []; set; } -} - /// Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class UserSettingsSetRequest @@ -6756,6 +7305,217 @@ internal sealed class UserSettingsSetRequest public JsonElement Settings { get; set; } } +/// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class GitHubRepositoryIdentity +{ + /// Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Repository name, without the owner prefix or the `.git` suffix. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Repository owner login (user or organization). + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; +} + +/// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubRepositoryAtPathResult +{ + /// Resolved repository identity, or null when the selected remote resolves to no GitHub host. + [JsonPropertyName("repository")] + public GitHubRepositoryIdentity? Repository { get; set; } +} + +/// Working-tree path whose owning GitHub repository should be resolved. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubRepositoryAtPathRequest +{ + /// Absolute path to a directory inside the git working tree to resolve. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} + +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersRequestIdResult +{ + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } +} + +/// A GitHub login the authenticated user may act as: their own account, or an organization they belong to. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnerOption +{ + /// The owner's GitHub login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Which kind of owner this is. The authenticated user's own account is always reported as `user`. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; +} + +/// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersListResult +{ + /// Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// The owners, on success: the authenticated user first, then the organizations they belong to. + [JsonPropertyName("owners")] + public IList? Owners { get; set; } + + /// A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. + [JsonPropertyName("throwError")] + public string? ThrowError { get; set; } + + /// A line the caller should log. Present only alongside `message`, and only for failures worth recording. + [JsonPropertyName("warning")] + public string? Warning { get; set; } +} + +/// Credential to list owners under, and the request id that makes the listing cancellable. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersListRequest +{ + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + [JsonPropertyName("authInfo")] + public JsonElement AuthInfo { get; set; } + + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } +} + +/// Whether the id named a running owner listing. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersCancelResult +{ + /// True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. + [JsonPropertyName("canceled")] + public bool Canceled { get; set; } +} + +/// The owner listing to abandon. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitHubOwnersCancelRequest +{ + /// Request id the listing was started with. + [JsonPropertyName("requestId")] + public long RequestId { get; set; } +} + +/// The remote the checked-out branch tracks. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitCurrentBranchRemoteResult +{ + /// Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("remote")] + public string Remote { get; set; } = string.Empty; +} + +/// Working-tree path a git query applies to. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitCwdRequest +{ + /// Absolute path to a directory inside the git working tree to query. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; +} + +/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionWorkingDirectoryContext +{ + /// Merge-base commit SHA (fork point from the remote default branch). + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } + + /// Current git branch name. + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + /// Current working directory path. + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; + + /// Root directory of the git repository, resolved via git rev-parse. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } + + /// Head commit of the current git branch. + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } + + /// Hosting platform type of the repository. + [JsonPropertyName("hostType")] + public SessionWorkingDirectoryContextHostType? HostType { get; set; } + + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com"). + [JsonPropertyName("repositoryHost")] + public string? RepositoryHost { get; set; } +} + +/// A GitHub repository one of a working tree's remotes points at. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitRemoteRepository +{ + /// GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// Repository name, without the owner. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Account or organization owning the repository. + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; + + /// Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. + [JsonPropertyName("remoteName")] + public string RemoteName { get; set; } = string.Empty; +} + +/// The GitHub repositories a working tree's remotes point at. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitReposFromRemotesResult +{ + /// One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + [JsonPropertyName("repositories")] + public IList Repositories { get => field ??= []; set; } +} + +/// Git working tree whose GitHub remotes should be listed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class GitReposFromRemotesRequest +{ + /// Absolute path to the root of the git working tree. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("gitRoot")] + public string GitRoot { get; set; } = string.Empty; +} + /// Validated device-managed settings discovered before a session exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ManagedSettingsReadResult @@ -7036,6 +7796,10 @@ public sealed class SessionFsSetProviderResult [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionFsSetProviderCapabilities { + /// Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes. + [JsonPropertyName("binary")] + public bool? Binary { get; set; } + /// Whether the provider supports SQLite query/exists operations. [JsonPropertyName("sqlite")] public bool? Sqlite { get; set; } @@ -8064,6 +8828,126 @@ internal sealed class SessionsEnrichMetadataRequest public IList Sessions { get => field ??= []; set; } } +/// The workspace record that was written. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsCreateWorkspaceResult +{ + /// The created workspace record, as JSON. + [JsonPropertyName("workspaceJson")] + public string WorkspaceJson { get; set; } = string.Empty; +} + +/// A working-directory context together with the client that produced it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionWorkingDirectoryContextWithClient +{ + /// Merge-base commit SHA. + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } + + /// Current git branch name. + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + /// Name of the client that created the session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// Current working directory path. + [JsonPropertyName("cwd")] + public string Cwd { get; set; } = string.Empty; + + /// Root directory of the git repository. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } + + /// Head commit of the current git branch. + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } + + /// Hosting platform type of the repository. + [JsonPropertyName("hostType")] + public string? HostType { get; set; } + + /// Repository identifier derived from the git remote URL. + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// Raw host string from the git remote URL. + [JsonPropertyName("repositoryHost")] + public string? RepositoryHost { get; set; } +} + +/// Identity, state location and starting context for a workspace record. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsCreateWorkspaceRequest +{ + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + [JsonPropertyName("context")] + public SessionWorkingDirectoryContextWithClient? Context { get; set; } + + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + [JsonPropertyName("convention")] + public string Convention { get; set; } = string.Empty; + + /// User-supplied display name for the workspace. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Session ID the workspace record belongs to. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + [JsonPropertyName("sessionStatePath")] + public string SessionStatePath { get; set; } = string.Empty; +} + +/// The workspace record on disk, omitted when the session has none. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsLoadWorkspaceResult +{ + /// The workspace record, as JSON. Omitted when the record does not exist. + [JsonPropertyName("workspaceJson")] + public string? WorkspaceJson { get; set; } +} + +/// Where the session's state lives, as a root directory and the session ID under it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsLoadWorkspaceRequest +{ + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Root directory every session's state directory sits under. + [JsonPropertyName("sessionsHome")] + public string SessionsHome { get; set; } = string.Empty; +} + +/// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsUpdateWorkspaceFieldsResult +{ +} + +/// Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionsUpdateWorkspaceFieldsRequest +{ + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + [JsonPropertyName("fieldsJson")] + public string FieldsJson { get; set; } = string.Empty; + + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Root directory every session's state directory sits under. + [JsonPropertyName("sessionsHome")] + public string SessionsHome { get; set; } = string.Empty; +} + /// Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionsReloadPluginHooksResult @@ -8111,47 +8995,6 @@ public sealed class SessionsSetAdditionalPluginsResult { } -/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstalledPlugin -{ - /// Path where the plugin is cached locally. - [JsonPropertyName("cache_path")] - public string? CachePath { get; set; } - - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } - - /// Installation timestamp. - [JsonPropertyName("installed_at")] - public string InstalledAt { get; set; } = string.Empty; - - /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — those synthesized at session start for a directory/local marketplace, whose cache_path points at the real plugin directory on disk rather than a copy under the installed-plugins cache. Its presence is what marks a record as live, and no record carrying it is ever written to the persisted installedPlugins key. - [JsonPropertyName("installed_from")] - public string? InstalledFrom { get; set; } - - /// Marketplace the plugin came from (empty string for direct repo installs). - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; - - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Source for direct repo installs (when marketplace is empty). - [JsonPropertyName("source")] - public JsonElement? Source { get; set; } - - /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. - [JsonPropertyName("source_sha")] - public string? SourceSha { get; set; } - - /// Version installed (if available). - [JsonPropertyName("version")] - public string? Version { get; set; } -} - /// Manager-wide additional plugins to register; replaces any previously-configured set. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class SessionsSetAdditionalPluginsRequest @@ -8639,81 +9482,135 @@ internal sealed class AgentRegistrySpawnRequest public AgentRegistrySpawnPermissionMode? PermissionMode { get; set; } } -/// Result of a OneAuth token acquisition. -/// Polymorphic base type discriminated by status. +/// Feature availability. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "status", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(EntraTokenAcquireResultOk), "ok")] -[JsonDerivedType(typeof(EntraTokenAcquireResultInteractionRequired), "interaction-required")] -public partial class EntraTokenAcquireResult +public sealed class ConnectorDiscoveryCapabilities { - /// The type discriminator. - [JsonPropertyName("status")] - public virtual string Status { get; set; } = string.Empty; -} + /// API version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } + + /// Availability. + [JsonPropertyName("availability")] + public ConnectorDiscoveryAvailability Availability { get; set; } + + /// Whether results are cached. + [JsonPropertyName("conditionalCache")] + public bool ConditionalCache { get; set; } + /// Whether accounts are selected by opaque ID. + [JsonPropertyName("opaqueAccountSelection")] + public bool OpaqueAccountSelection { get; set; } +} -/// The ok variant of . +/// Account metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EntraTokenAcquireResultOk : EntraTokenAcquireResult +public sealed class ConnectorDiscoveryAuthInfo { - /// - [JsonIgnore] - public override string Status => "ok"; + /// Host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Opaque access token. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } + /// Login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Opaque OneAuth account id, when supplied by the broker. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + /// Authentication type. + [JsonPropertyName("type")] + public AuthInfoType Type { get; set; } +} + +/// Eligible account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryAccount +{ + /// Opaque account ID. [JsonPropertyName("accountId")] - public string? AccountId { get; set; } + public string AccountId { get; set; } = string.Empty; - /// Expiry as milliseconds since Unix epoch, when supplied by OneAuth. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiresOnTimestamp")] - public double? ExpiresOnTimestamp { get; set; } + /// Account metadata. + [JsonPropertyName("authInfo")] + public ConnectorDiscoveryAuthInfo AuthInfo { get => field ??= new(); set; } } -/// The interaction-required variant of . +/// Eligible accounts. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EntraTokenAcquireResultInteractionRequired : EntraTokenAcquireResult +public sealed class ConnectorDiscoveryAccountList { - /// - [JsonIgnore] - public override string Status => "interaction-required"; + /// Eligible accounts. + [JsonPropertyName("accounts")] + public IList Accounts { get => field ??= []; set; } + + /// Availability. + [JsonPropertyName("availability")] + public ConnectorDiscoveryAvailability Availability { get; set; } } -/// OneAuth token request supplied by a trusted host application. +/// Entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EntraTokenAcquireRequest +public sealed class ConnectorDiscoveryCatalogEntry { - /// Previously rejected token that OneAuth must bypass during renewal. - [JsonPropertyName("accessTokenToRenew")] - public string? AccessTokenToRenew { get; set; } + /// Description. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Public client application id. - [JsonPropertyName("clientId")] - public string ClientId { get; set; } = string.Empty; + /// Display name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Whether the broker may show interaction. - [JsonPropertyName("interaction")] - public EntraTokenInteraction Interaction { get; set; } + /// Logo. + [JsonPropertyName("logo")] + public string? Logo { get; set; } - /// Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. - [JsonPropertyName("redirectUri")] - public string RedirectUri { get; set; } = string.Empty; + /// Name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Release tag. + [JsonPropertyName("releaseTag")] + public string? ReleaseTag { get; set; } + + /// Status. + [JsonPropertyName("status")] + public ConnectorCatalogStatus Status { get; set; } + + /// Tier. + [JsonPropertyName("tier")] + public string? Tier { get; set; } +} + +/// Entries for the selected account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDiscoveryCatalogResult +{ + /// Opaque account ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Entries. + [JsonPropertyName("connectors")] + public IList Connectors { get => field ??= []; set; } + + /// Refresh time in Unix epoch milliseconds. + [JsonPropertyName("refreshedAtMs")] + public long RefreshedAtMs { get; set; } - /// Exact delegated scopes to request. - [JsonPropertyName("scopes")] - public IList Scopes { get => field ??= []; set; } + /// Revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } +} - /// Tenant id or tenant selector, such as common or organizations. - [JsonPropertyName("tenantId")] - public string TenantId { get; set; } = string.Empty; +/// Selected account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ConnectorDiscoveryAccountRequest +{ + /// Opaque account ID. + [RegularExpression("^\\S+$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(2048)] + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; } /// Identifies the target session. @@ -9079,2528 +9976,2684 @@ internal sealed class LogRequest public string? Url { get; set; } } -/// Managed sandbox enforcement state for a session. +/// Adapter-declared policy that tells clients whether discovery may run automatically. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxEnforcementStatus +public sealed class ModelProviderAutomaticDiscoveryPolicy { - /// Whether an enforcement failure has permanently blocked the session. - [JsonPropertyName("blocked")] - public bool Blocked { get; set; } + /// Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + [JsonPropertyName("mode")] + public ModelProviderAutomaticDiscoveryMode Mode { get; set; } - /// The first sandbox enforcement failure that blocked the session. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Maximum network scope used by this adapter during discovery. + [JsonPropertyName("networkScope")] + public ModelProviderDiscoveryNetworkScope NetworkScope { get; set; } - /// Whether the effective managed policy requires an available sandbox backend. - [JsonPropertyName("required")] - public bool Required { get; set; } -} + /// True when discovery requires non-null caller input. Omission or null is rejected before adapter execution. When false, omitted or null input selects adapter defaults without schema validation. + [JsonPropertyName("requiresInput")] + public bool RequiresInput { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSandboxGetEnforcementStatusRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. + [JsonPropertyName("requiresTrust")] + public bool RequiresTrust { get; set; } } -/// Result of attempting to disable sandboxing for the current session. +/// An operation supported by a model-provider adapter. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxDisableForSessionResult +public sealed class ModelProviderAdapterOperationDescriptor { - /// The authoritative sandbox enabled state after the operation. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported on discover. No external references are resolved. Omitted or null input selects defaults when requiresInput is false. Without a schema, the adapter validates supplied input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } - /// Whether this call resolved the pending request and applied the session opt-out. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Optional informational context describing how and where the permission decision was made. This does not affect permission behavior. +/// Contributor attribution, independent of routing identity and authorization. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionDecisionContext +public sealed class ModelProviderAttribution { - /// Disposition of the permission request as observed by the responding client. - [JsonPropertyName("outcome")] - public PermissionDecisionOutcome Outcome { get; set; } + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { 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; } + /// Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. + [JsonPropertyName("ownerId")] + public string? OwnerId { get; set; } - /// Controlled reason or actor responsible for the response. + /// Kind of component that supplied the adapter. Attribution does not confer authority. [JsonPropertyName("source")] - public PermissionDecisionSource Source { get; set; } - - /// Client surface that submitted the response. - [JsonPropertyName("surface")] - public PermissionDecisionSurface Surface { get; set; } + public ModelProviderProvenanceSource Source { get; set; } } -/// Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. +/// A normalized model-provider adapter in the session's effective catalog. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SandboxDisableForSessionRequest +public sealed class ModelProviderAdapterDescriptor { - /// Optional attribution for the permission decision. - [JsonPropertyName("decisionContext")] - public PermissionDecisionContext? DecisionContext { get; set; } - - /// Identifier of the exact pending sandbox-bypass permission request that authorized the session opt-out. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Adapter-declared policy for passive and automatic discovery. + [JsonPropertyName("automaticDiscovery")] + public ModelProviderAutomaticDiscoveryPolicy AutomaticDiscovery { get => field ??= new(); set; } -/// Result of accepting a sandbox path grant. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxGrantPathForRequestResult -{ - /// Whether this call resolved the pending request and added the path to the session's sandbox policy. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Human-readable provider name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; -/// Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SandboxGrantPathForRequestRequest -{ - /// Optional attribution for the permission decision. - [JsonPropertyName("decisionContext")] - public PermissionDecisionContext? DecisionContext { get; set; } + /// Operations supported by this provider adapter. + [JsonPropertyName("operations")] + public IList Operations { get => field ??= []; set; } - /// Identifier of the exact pending sandbox escalation permission request whose sandboxPathGrant to accept. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Attribution for the adapter itself. + [JsonPropertyName("provenance")] + public ModelProviderAttribution Provenance { get => field ??= new(); set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. + [JsonPropertyName("providerKind")] + public string ProviderKind { get; set; } = string.Empty; } -/// Authentication status and account metadata for the session. +/// Normalized model-provider adapter definitions available to the session, not discovered instances. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionAuthStatus +public sealed class ModelProviderAdapterCatalog { - /// Authentication type. - [JsonPropertyName("authType")] - public AuthInfoType? AuthType { get; set; } - - /// Copilot plan tier (e.g., individual_pro, business). - [JsonPropertyName("copilotPlan")] - public string? CopilotPlan { get; set; } - - /// Authentication host URL. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("host")] - public string? Host { get; set; } - - /// Whether the session has resolved authentication. - [JsonPropertyName("isAuthenticated")] - public bool IsAuthenticated { get; set; } - - /// Authenticated login/username, if available. - [JsonPropertyName("login")] - public string? Login { get; set; } - - /// Human-readable authentication status description. - [JsonPropertyName("statusMessage")] - public string? StatusMessage { get; set; } + /// Available provider adapters ordered by adapterId. + [JsonPropertyName("providers")] + public IList Providers { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetStatusRequest +internal sealed class SessionProvidersGetCatalogRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the credential update succeeded. +/// Attribution for the adapter that produced a provider row. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSetCredentialsResult +public sealed class ModelProviderProvenance { - /// Whether the session ended up with a populated `copilotUser` for the installed credentials. `true` when the supplied credential already carried `copilotUser` or it was successfully re-resolved server-side. `false` when the credential is installed without `copilotUser` — either re-resolution failed, or the variant cannot be re-resolved from the credential alone (only the raw-token variants `token`, `env`, and `gh-cli` can). In both `false` cases the token swap still applied, but plan/quota/billing metadata is degraded. Present whenever a credential was supplied; omitted only when no credential was supplied (no-op call). - [JsonPropertyName("copilotUserResolved")] - public bool? CopilotUserResolved { get; set; } + /// Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { 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(global::GitHub.Copilot.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; -} + /// Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. + [JsonPropertyName("ownerId")] + public string? OwnerId { get; set; } + /// Descriptive provider family that produced this row; not a routing key. + [JsonPropertyName("providerKind")] + public string ProviderKind { 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 . + /// Kind of component that supplied the adapter. + [JsonPropertyName("source")] + public ModelProviderProvenanceSource Source { get; set; } +} + +/// Serializable reference to a discovered provider instance. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoHmac : SettableAuthInfo +public sealed class ModelProviderInstanceReference { - /// - [JsonIgnore] - public override string Type => "hmac"; + /// Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; - /// 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; } + /// Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// HMAC secret used to sign requests. - [JsonPropertyName("hmac")] - public required string Hmac { get; set; } + /// Absolute provider management URI. The adapter validates normalization, supported schemes, and permission to access it against its bound configuration; a reference does not grant authority. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("managementEndpoint")] + public string ManagementEndpoint { get; set; } = string.Empty; - /// Authentication host. HMAC auth always targets the public GitHub host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Descriptive provider family. Must match the selected adapter; not a routing key. + [JsonPropertyName("providerKind")] + public string ProviderKind { get; set; } = string.Empty; } -/// 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 . +/// A normalized model-provider instance discovered by the runtime. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoEnv : SettableAuthInfo +public sealed class ModelProviderInstance { - /// - [JsonIgnore] - public override string Type => "env"; + /// Human-readable instance name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// 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; } + /// Inference API endpoint when the provider exposes one separately from its management endpoint. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("inferenceEndpoint")] + public string? InferenceEndpoint { get; set; } - /// Name of the environment variable the token was sourced from. - [JsonPropertyName("envVar")] - public required string EnvVar { get; set; } + /// Transport to use for inference against this instance. + [JsonPropertyName("inferenceTransport")] + public ProviderEndpointTransport? InferenceTransport { get; set; } - /// Authentication host (e.g. https://github.com or a GHES host). - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Provider family to use for inference against this instance. + [JsonPropertyName("inferenceType")] + public ProviderEndpointType? InferenceType { 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; } + /// Wire API to use for inference against this instance, when required by the provider family. + [JsonPropertyName("inferenceWireApi")] + public ProviderEndpointWireApi? InferenceWireApi { get; set; } - /// The token value itself. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { get; set; } + /// Attribution for the adapter that produced this instance. + [JsonPropertyName("provenance")] + public ModelProviderProvenance Provenance { get => field ??= new(); set; } + + /// Self-contained reference for subsequent provider operations. + [JsonPropertyName("reference")] + public ModelProviderInstanceReference Reference { get => field ??= new(); set; } } -/// Token authentication accepted by session.gitHubAuth.setCredentials. -/// The token variant of . +/// Typed provider-operation outcome. Use the code for control flow and the optional message for display. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoToken : SettableAuthInfo +public sealed class ModelProviderOperationOutcome { - /// - [JsonIgnore] - public override string Type => "token"; + /// Machine-readable operation outcome. + [JsonPropertyName("code")] + public ModelProviderOperationOutcomeCode Code { 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; } + /// Human-readable detail for non-success outcomes. + [JsonPropertyName("message")] + public string? Message { get; set; } +} - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } +/// Provider instances found by a discovery operation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ModelProviderDiscoverResult +{ + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + [JsonPropertyName("instances")] + public IList Instances { get => field ??= []; set; } - /// The token value itself. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { get; set; } + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } } -/// Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host. -/// The copilot-api-token variant of . +/// Provider discovery parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoCopilotApiToken : SettableAuthInfo +internal sealed class ModelProviderDiscoverRequest { - /// - [JsonIgnore] - public override string Type => "copilot-api-token"; + /// Opaque adapter identity returned by `session.providers.getCatalog`. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; - /// 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; } + /// Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Authentication host (always the public GitHub host). - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Authentication-info variant for OAuth user auth, with host and login; the token remains in the runtime secret store. -/// The user variant of . +/// Current health information for a provider instance. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoUser : SettableAuthInfo +public sealed class ModelProviderStatus { - /// - [JsonIgnore] - public override string Type => "user"; + /// Normalized provider instance. + [JsonPropertyName("instance")] + public ModelProviderInstance Instance { get => field ??= new(); 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; } + /// Typed operation outcome. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; - /// OAuth user login. - [JsonPropertyName("login")] - public required string Login { get; set; } + /// Provider-reported version. + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. -/// The gh-cli variant of . +/// Provider status request parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoGhCli : SettableAuthInfo +internal sealed class ModelProviderGetStatusRequest { - /// - [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; } + /// Provider instance reference returned by discovery. + [JsonPropertyName("instance")] + public ModelProviderInstanceReference Instance { get => field ??= new(); set; } - /// The token returned by `gh auth token`. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// 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 . +/// Provider-reported model artifact metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoApiKey : SettableAuthInfo +public sealed class ModelArtifactDetails { - /// - [JsonIgnore] - public override string Type => "api-key"; + /// Provider-reported model architecture. + [JsonPropertyName("architecture")] + public string? Architecture { get; set; } - /// The API key. Treat as a secret. - [JsonPropertyName("apiKey")] - public required string ApiKey { get; set; } + /// Provider-reported model families. + [JsonPropertyName("families")] + public IList? Families { 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; } + /// Primary model family. + [JsonPropertyName("family")] + public string? Family { get; set; } - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Artifact format, such as `gguf`. + [JsonPropertyName("format")] + public string? Format { get; set; } + + /// Provider-reported parameter count label. + [JsonPropertyName("parameterSize")] + public string? ParameterSize { get; set; } + + /// Provider-reported quantization label. + [JsonPropertyName("quantization")] + public string? Quantization { get; set; } + + /// Provider-reported tokenizer. + [JsonPropertyName("tokenizer")] + public string? Tokenizer { get; set; } } -/// New auth credentials to install on the session. Omit to leave credentials unchanged. +/// A non-fatal provider observation warning. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSetCredentialsParams +public sealed class ModelProviderWarning { - /// 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 SettableAuthInfo? Credentials { get; set; } + /// Machine-readable warning code. + [JsonPropertyName("code")] + public string Code { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Human-readable warning message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; } -/// Credential-free authentication identity safe to expose to hosts and user interfaces. +/// A model offered for agent conversations. Missing capability metadata does not disqualify a candidate. Models known to be incompatible, such as embedding-only models, are excluded by the adapter. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthIdentity +public sealed class DiscoveredModel { - /// Snapshot of the authenticated user's Copilot subscription info, if known. - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } + /// Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. + [JsonPropertyName("capabilities")] + public ModelCapabilities Capabilities { get => field ??= new(); set; } - /// Name of the environment variable that supplied the credential, when applicable. - [JsonPropertyName("envVar")] - public string? EnvVar { get; set; } + /// Provider-reported model artifact details. + [JsonPropertyName("details")] + public ModelArtifactDetails Details { get => field ??= new(); set; } - /// Authentication host. - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; + /// Provider-reported artifact digest. + [JsonPropertyName("digest")] + public string? Digest { get; set; } - /// Authenticated login, when available. - [JsonPropertyName("login")] - public string? Login { get; set; } + /// Provider-native model identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. - [JsonPropertyName("registrationId")] - public string? RegistrationId { get; set; } + /// Provider-reported last-modified timestamp. + [JsonPropertyName("modifiedAt")] + public DateTimeOffset? ModifiedAt { get; set; } - /// Authentication type. - [JsonPropertyName("type")] - public AuthInfoType Type { get; set; } + /// Provider-reported display name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Attribution for the adapter that produced this model row. + [JsonPropertyName("provenance")] + public ModelProviderProvenance Provenance { get => field ??= new(); set; } + + /// Provider-reported artifact size in bytes. + [JsonPropertyName("sizeBytes")] + public long? SizeBytes { get; set; } + + /// Non-fatal warnings encountered while enriching this model. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// Identifies the target session. +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetCurrentAuthInfoRequest +public sealed class DiscoveredModelList { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Provider-native models in provider order. + [JsonPropertyName("models")] + public IList Models { get => field ??= []; set; } + + /// Typed operation outcome. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } } -/// Identifies the target session. +/// Provider model inventory request parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetAllAuthAvailableRequest +internal sealed class ModelProviderModelsListRequest { + /// Provider instance reference returned by discovery. + [JsonPropertyName("instance")] + public ModelProviderInstanceReference Instance { get => field ??= new(); set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Vision-specific limits. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthRefreshCopilotUserRequest +public sealed class ModelCapabilitiesOverrideLimitsVision { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Maximum image size in bytes. + [JsonPropertyName("max_prompt_image_size")] + public long? MaxPromptImageSize { get; set; } + + /// Maximum number of images per prompt. + [JsonPropertyName("max_prompt_images")] + public long? MaxPromptImages { get; set; } + + /// MIME types the model accepts. + [JsonPropertyName("supported_media_types")] + public IList? SupportedMediaTypes { get; set; } } -/// Internal GitHub login parameters. +/// Token limits for prompts, outputs, and context window. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAuthLoginRequest +public sealed class ModelCapabilitiesOverrideLimits { - /// GitHub host URL. - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; - - /// GitHub login. - [JsonPropertyName("login")] - public string Login { get; set; } = string.Empty; + /// Maximum total context window size in tokens. + [JsonPropertyName("max_context_window_tokens")] + public long? MaxContextWindowTokens { get; set; } - /// Whether to persist the token after login. - [JsonPropertyName("persist")] - public bool? Persist { get; set; } + /// Maximum number of output/completion tokens. + [JsonPropertyName("max_output_tokens")] + public long? MaxOutputTokens { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Maximum number of prompt/input tokens. + [JsonPropertyName("max_prompt_tokens")] + public long? MaxPromptTokens { get; set; } - /// GitHub authentication token. - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; + /// Vision-specific limits. + [JsonPropertyName("vision")] + public ModelCapabilitiesOverrideLimitsVision? Vision { get; set; } } -/// Parameters for switching the session's active authentication. +/// Feature flags indicating what the model supports. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAuthSwitchRequest +public sealed class ModelCapabilitiesOverrideSupports { - /// Authentication information to activate. - [JsonPropertyName("authInfo")] - public AuthInfo AuthInfo { get => field ??= new(); set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; - - /// Optional token paired with the authentication information. - [JsonPropertyName("token")] - public string? Token { get; set; } -} + /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). + [JsonPropertyName("adaptive_thinking")] + public AdaptiveThinkingSupport? AdaptiveThinking { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthLogoutRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether this model supports reasoning effort configuration. + [JsonPropertyName("reasoningEffort")] + public bool? ReasoningEffort { get; set; } -/// Parameters identifying a GitHub authentication to log out. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAuthLogoutUserRequest -{ - /// Authentication information to log out. - [JsonPropertyName("authInfo")] - public AuthInfo AuthInfo { get => field ??= new(); set; } + /// Whether this model supports canonical tool calling. + [JsonPropertyName("toolCalls")] + public bool? ToolCalls { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether this model supports vision/image input. + [JsonPropertyName("vision")] + public bool? Vision { get; set; } } -/// Validation error from an authentication attempt. +/// Optional capability overrides (vision, tool_calls, reasoning, etc.). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthValidationError +public sealed class ModelCapabilitiesOverride { - /// Optional message returned by GitHub. - [JsonPropertyName("githubMessage")] - public string? GitHubMessage { get; set; } + /// Token limits for prompts, outputs, and context window. + [JsonPropertyName("limits")] + public ModelCapabilitiesOverrideLimits? Limits { get; set; } - /// Authentication validation error message. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Feature flags indicating what the model supports. + [JsonPropertyName("supports")] + public ModelCapabilitiesOverrideSupports? Supports { get; set; } } -/// Identifies the target session. +/// RPC data type for ProtocolSystemMessageAppendConfig operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthLastAuthErrorsRequest +public sealed class ProtocolSystemMessageAppendConfig { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Text appended to the standard system prompt. + [JsonPropertyName("content")] + public string? Content { get; set; } -/// The enumerated collection, keyed by the same selector as the query. -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AuthEnumerateValueAccounts), "accounts")] -[JsonDerivedType(typeof(AuthEnumerateValueProviders), "providers")] -public partial class AuthEnumerateValue -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// Append-mode discriminator. Omission also selects append mode. + [JsonPropertyName("mode")] + public ProtocolAppendMode? Mode { get; set; } } - -/// One signed-in account in the roster forest. +/// RPC data type for SystemMessageBlock operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AccountStatus +public sealed class SystemMessageBlock { - /// Whether this is the active account. - [JsonPropertyName("active")] - public bool Active { get; set; } - - /// Opaque id of the account this one was derived from (e.g. an EMU account's base Entra identity); absent for a root account. Matches the base identity account's selectionId, forming the derivation edge. - [JsonPropertyName("derivedFrom")] - public string? DerivedFrom { get; set; } - - /// Authentication host URL. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; - - /// The provider kind of this account. - [JsonPropertyName("kind")] - public AccountKind Kind { get; set; } - - /// Authenticated login/username. - [JsonPropertyName("login")] - public string Login { get; set; } = string.Empty; - - /// Opaque selection id used to switch to, or log out, this account. - [JsonPropertyName("selectionId")] - public string SelectionId { get; set; } = string.Empty; -} + /// Whether providers with explicit prompt caching should place a cache breakpoint after this block. + [JsonPropertyName("cacheBreakpoint")] + public bool? CacheBreakpoint { get; set; } -/// The accounts variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthEnumerateValueAccounts : AuthEnumerateValue -{ - /// - [JsonIgnore] - public override string Kind => "accounts"; + /// Text content for this system-message block. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// The signed-in account forest; empty when not logged in. - [JsonPropertyName("items")] - public required IList Items { get; set; } + /// Whether the block is static and may be cached independently of dynamic prompt content. + [JsonPropertyName("isStatic")] + public bool? IsStatic { get; set; } } -/// A provider offered for interactive login. +/// RPC data type for ProtocolSystemMessageReplaceConfig operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderDescriptor +public sealed class ProtocolSystemMessageReplaceConfig { - /// Whether this provider is currently available to sign in with. - [JsonPropertyName("available")] - public bool Available { get; set; } + /// Complete replacement system-message text. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// The neutral provider kind. - [JsonPropertyName("kind")] - public LoginProviderKind Kind { get; set; } + /// Optional structured blocks corresponding to the replacement content. + [JsonPropertyName("contentBlocks")] + public IList? ContentBlocks { get; set; } - /// Human-readable menu label, owned by the runtime so every consumer renders identical text. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; + /// Replace-mode discriminator. + [JsonPropertyName("mode")] + public ProtocolReplaceMode Mode { get; set; } } -/// The providers variant of . +/// RPC data type for ProtocolStaticSectionOverride operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthEnumerateValueProviders : AuthEnumerateValue +public sealed class ProtocolStaticSectionOverride { - /// - [JsonIgnore] - public override string Kind => "providers"; + /// Declarative operation applied to the section. + [JsonPropertyName("action")] + public ProtocolStaticSectionAction Action { get; set; } - /// The providers offered for interactive login. - [JsonPropertyName("items")] - public required IList Items { get; set; } + /// Optional content used by replace, append, and prepend operations. + [JsonPropertyName("content")] + public string? Content { get; set; } } -/// Selects which accounts collection to enumerate. A no-arg selector is the empty-payload variant. -/// Polymorphic base type discriminated by kind. +/// Polymorphic base type discriminated by action. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", + TypeDiscriminatorPropertyName = "action", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AuthEnumerateQueryAccounts), "accounts")] -[JsonDerivedType(typeof(AuthEnumerateQueryProviders), "providers")] -public partial class AuthEnumerateQuery +[JsonDerivedType(typeof(ProtocolMarkerSectionOverrideTransform), "transform")] +[JsonDerivedType(typeof(ProtocolMarkerSectionOverridePreserve), "preserve")] +public partial class ProtocolMarkerSectionOverride { /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + [JsonPropertyName("action")] + public virtual string Action { get; set; } = string.Empty; } -/// The accounts variant of . +/// The transform variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthEnumerateQueryAccounts : AuthEnumerateQuery +public partial class ProtocolMarkerSectionOverrideTransform : ProtocolMarkerSectionOverride { /// [JsonIgnore] - public override string Kind => "accounts"; + public override string Action => "transform"; } -/// The providers variant of . +/// The preserve variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthEnumerateQueryProviders : AuthEnumerateQuery +public partial class ProtocolMarkerSectionOverridePreserve : ProtocolMarkerSectionOverride { /// [JsonIgnore] - public override string Kind => "providers"; - - /// Whether an interactive Entra broker is available on the host; gates Entra availability in the returned list. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("brokerAvailable")] - public bool? BrokerAvailable { get; set; } -} - -/// Enumerate request carrying the typed collection query. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AccountsEnumerateRequest -{ - /// Which typed accounts collection to enumerate. - [JsonPropertyName("query")] - public AuthEnumerateQuery Query { get => field ??= new(); set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} - -/// The read result, keyed by the same selector as the query. -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AuthReadValueActiveAccount), "activeAccount")] -[JsonDerivedType(typeof(AuthReadValueStatus), "status")] -[JsonDerivedType(typeof(AuthReadValueLastErrors), "lastErrors")] -public partial class AuthReadValue -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + public override string Action => "preserve"; } - -/// The activeAccount variant of . +/// JSON union data type for ProtocolSectionOverride. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadValueActiveAccount : AuthReadValue +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSectionOverride { - /// - [JsonIgnore] - public override string Kind => "activeAccount"; + /// Gets the value when this instance contains . + public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } - /// The active account, or absent when not logged in. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("account")] - public AccountStatus? Account { get; set; } -} + /// Gets the value when this instance contains . + public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } -/// Neutral authentication status summary. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthStatusDto -{ - /// Number of signed-in accounts in the roster. - [JsonPropertyName("accountCount")] - public long AccountCount { get; set; } + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolStaticSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolStaticSectionOverride = value; + } - /// Active account host, if authenticated. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("activeHost")] - public string? ActiveHost { get; set; } + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); - /// Active account login, if authenticated. - [JsonPropertyName("activeLogin")] - public string? ActiveLogin { get; set; } + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolMarkerSectionOverride = value; + } - /// Copilot plan tier of the active account, if known. - [JsonPropertyName("copilotPlan")] - public string? CopilotPlan { get; set; } + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); - /// Whether the session has resolved authentication. - [JsonPropertyName("isAuthenticated")] - public bool IsAuthenticated { get; set; } -} + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProtocolSectionOverride Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + throw new JsonException("Expected JSON object for ProtocolSectionOverride."); + } -/// The status variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadValueStatus : AuthReadValue -{ - /// - [JsonIgnore] - public override string Kind => "status"; + using var document = JsonDocument.ParseValue(ref reader); + var element = document.RootElement; + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "replace" || element.GetProperty("action").GetString() == "remove" || element.GetProperty("action").GetString() == "append" || element.GetProperty("action").GetString() == "prepend"))) + { + var protocolStaticSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolStaticSectionOverride); + return protocolStaticSectionOverride is null ? throw new JsonException("Expected ProtocolStaticSectionOverride value.") : new ProtocolSectionOverride(protocolStaticSectionOverride); + } + if ((element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "transform")) || element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "preserve")))) + { + var protocolMarkerSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolMarkerSectionOverride); + return protocolMarkerSectionOverride is null ? throw new JsonException("Expected ProtocolMarkerSectionOverride value.") : new ProtocolSectionOverride(protocolMarkerSectionOverride); + } - /// The neutral authentication status summary. - [JsonPropertyName("status")] - public required AuthStatusDto Status { get; set; } -} + throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); + } -/// The lastErrors variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadValueLastErrors : AuthReadValue -{ - /// - [JsonIgnore] - public override string Kind => "lastErrors"; + /// + public override void Write(Utf8JsonWriter writer, ProtocolSectionOverride value, JsonSerializerOptions options) + { + if (value.ProtocolStaticSectionOverride is { } protocolStaticSectionOverride) + { + JsonSerializer.Serialize(writer, protocolStaticSectionOverride, RpcJsonContext.Default.ProtocolStaticSectionOverride); + return; + } + if (value.ProtocolMarkerSectionOverride is { } protocolMarkerSectionOverride) + { + JsonSerializer.Serialize(writer, protocolMarkerSectionOverride, RpcJsonContext.Default.ProtocolMarkerSectionOverride); + return; + } - /// Validation errors from the most recent authentication attempt. - [JsonPropertyName("errors")] - public required IList Errors { get; set; } + throw new JsonException("No ProtocolSectionOverride variant value is set."); + } + } } -/// Selects which typed accounts datum to read. -/// Polymorphic base type discriminated by kind. +/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AuthReadQueryActiveAccount), "activeAccount")] -[JsonDerivedType(typeof(AuthReadQueryStatus), "status")] -[JsonDerivedType(typeof(AuthReadQueryLastErrors), "lastErrors")] -public partial class AuthReadQuery +public sealed class ProtocolSystemMessageCustomizeConfig { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Text appended after the customized sections. + [JsonPropertyName("content")] + public string? Content { get; set; } + /// Customize-mode discriminator. + [JsonPropertyName("mode")] + public ProtocolCustomizeMode Mode { get; set; } -/// The activeAccount variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadQueryActiveAccount : AuthReadQuery -{ - /// - [JsonIgnore] - public override string Kind => "activeAccount"; + /// Named standard-prompt section overrides. + [JsonPropertyName("sections")] + public IDictionary? Sections { get; set; } } -/// The status variant of . +/// JSON union data type for ProtocolSystemMessageConfig. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadQueryStatus : AuthReadQuery +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSystemMessageConfig { - /// - [JsonIgnore] - public override string Kind => "status"; -} + /// Gets the value when this instance contains . + public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } -/// The lastErrors variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadQueryLastErrors : AuthReadQuery -{ - /// - [JsonIgnore] - public override string Kind => "lastErrors"; -} + /// Gets the value when this instance contains . + public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } -/// Read request carrying the typed datum query. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AccountsGetRequest -{ - /// Which typed accounts datum to read. - [JsonPropertyName("query")] - public AuthReadQuery Query { get => field ??= new(); set; } + /// Gets the value when this instance contains . + public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageAppendConfig = value; + } -/// Result of a non-interactive accounts mutation. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthWriteResult -{ - /// For a logout, whether other signed-in accounts remain. - [JsonPropertyName("moreUsers")] - public bool? MoreUsers { get; set; } + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); - /// Whether the mutation was applied. - [JsonPropertyName("ok")] - public bool Ok { get; set; } -} + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageReplaceConfig = value; + } -/// One non-interactive accounts mutation command (the selector is fused with its typed args). -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AuthWriteSwitchActive), "switchActive")] -[JsonDerivedType(typeof(AuthWriteLogout), "logout")] -[JsonDerivedType(typeof(AuthWriteSetCredentials), "setCredentials")] -public partial class AuthWrite -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) => new(value); + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageCustomizeConfig = value; + } -/// The switchActive variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthWriteSwitchActive : AuthWrite -{ - /// - [JsonIgnore] - public override string Kind => "switchActive"; + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); - /// Opaque selection id of the account to make active. - [JsonPropertyName("selectionId")] - public required string SelectionId { get; set; } -} + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ProtocolSystemMessageConfig Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + throw new JsonException("Expected JSON object for ProtocolSystemMessageConfig."); + } -/// The logout variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthWriteLogout : AuthWrite -{ - /// - [JsonIgnore] - public override string Kind => "logout"; + using var document = JsonDocument.ParseValue(ref reader); + var element = document.RootElement; + if (element.ValueKind == JsonValueKind.Object && (!element.TryGetProperty("mode", out _) || (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "append")))) + { + var protocolSystemMessageAppendConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); + return protocolSystemMessageAppendConfig is null ? throw new JsonException("Expected ProtocolSystemMessageAppendConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageAppendConfig); + } + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "replace"))) + { + var protocolSystemMessageReplaceConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); + return protocolSystemMessageReplaceConfig is null ? throw new JsonException("Expected ProtocolSystemMessageReplaceConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageReplaceConfig); + } + if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "customize"))) + { + var protocolSystemMessageCustomizeConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); + return protocolSystemMessageCustomizeConfig is null ? throw new JsonException("Expected ProtocolSystemMessageCustomizeConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageCustomizeConfig); + } - /// Opaque selection id of the account to log out; absent logs out the active account. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("selectionId")] - public string? SelectionId { get; set; } + throw new JsonException("JSON value did not match any ProtocolSystemMessageConfig variant."); + } + + /// + public override void Write(Utf8JsonWriter writer, ProtocolSystemMessageConfig value, JsonSerializerOptions options) + { + if (value.ProtocolSystemMessageAppendConfig is { } protocolSystemMessageAppendConfig) + { + JsonSerializer.Serialize(writer, protocolSystemMessageAppendConfig, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); + return; + } + if (value.ProtocolSystemMessageReplaceConfig is { } protocolSystemMessageReplaceConfig) + { + JsonSerializer.Serialize(writer, protocolSystemMessageReplaceConfig, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); + return; + } + if (value.ProtocolSystemMessageCustomizeConfig is { } protocolSystemMessageCustomizeConfig) + { + JsonSerializer.Serialize(writer, protocolSystemMessageCustomizeConfig, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); + return; + } + + throw new JsonException("No ProtocolSystemMessageConfig variant value is set."); + } + } } -/// The setCredentials variant of . +/// A BYOK model definition referencing a named provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthWriteSetCredentials : AuthWrite +public sealed class ProviderModelConfig { - /// - [JsonIgnore] - public override string Kind => "setCredentials"; + /// Optional capability overrides (vision, tool_calls, reasoning, etc.). + [JsonPropertyName("capabilities")] + public ModelCapabilitiesOverride? Capabilities { get; set; } - /// Authentication host URL. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Login/username for the credential. - [JsonPropertyName("login")] - public required string Login { get; set; } + /// Maximum context window tokens for the model. + [JsonPropertyName("maxContextWindowTokens")] + public double? MaxContextWindowTokens { get; set; } - /// GitHub authentication token to install. - [JsonPropertyName("token")] - public required string Token { get; set; } -} + /// Maximum output tokens for the model. + [JsonPropertyName("maxOutputTokens")] + public double? MaxOutputTokens { get; set; } -/// Mutation request carrying the typed write command. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AccountsSetRequest -{ - /// The non-interactive mutation command to apply. - [JsonPropertyName("command")] - public AuthWrite Command { get => field ??= new(); set; } + /// Maximum prompt/input tokens for the model. + [JsonPropertyName("maxPromptTokens")] + public double? MaxPromptTokens { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. + [JsonPropertyName("metadata")] + public IDictionary? Metadata { get; set; } + + /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } + + /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Name of the configured provider that serves this model. + [JsonPropertyName("provider")] + public string Provider { get; set; } = string.Empty; + + /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. + [JsonPropertyName("systemMessage")] + public ProtocolSystemMessageConfig? SystemMessage { get; set; } + + /// The model name sent to the provider API for inference. Defaults to `id`. + [JsonPropertyName("wireModel")] + public string? WireModel { get; set; } } -/// One step in an interactive login flow. The consumer acts on the step and calls advance to proceed. Browser-open is encoded as two distinct steps by design: `open-url` is CONSUMER-driven (the provider surfaces the authorize URL and the consumer opens it — github.com/GHEC web), while `needs-interaction` is PROVIDER-driven (the provider opens the browser or broker UI itself and does not surface a URL — Entra). -/// Polymorphic base type discriminated by kind. +/// Azure-specific provider options. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AuthLoginStepOpenUrl), "open-url")] -[JsonDerivedType(typeof(AuthLoginStepInputRequired), "input-required")] -[JsonDerivedType(typeof(AuthLoginStepAwaiting), "awaiting")] -[JsonDerivedType(typeof(AuthLoginStepNeedsInteraction), "needs-interaction")] -[JsonDerivedType(typeof(AuthLoginStepCompleted), "completed")] -[JsonDerivedType(typeof(AuthLoginStepError), "error")] -public partial class AuthLoginStep +public sealed class ProviderConfigAzure { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + [JsonPropertyName("apiVersion")] + public string? ApiVersion { get; set; } } - -/// The open-url variant of . +/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepOpenUrl : AuthLoginStep +public sealed class NamedProviderConfig { - /// - [JsonIgnore] - public override string Kind => "open-url"; + /// Static API key used to authenticate provider requests. + [JsonPropertyName("apiKey")] + public string? ApiKey { get; set; } - /// Authorize URL the consumer should open in a browser (consumer-driven browser-open). - [JsonPropertyName("url")] - public required string Url { get; set; } -} + /// Azure authentication configuration for the provider. + [JsonPropertyName("azure")] + public ProviderConfigAzure? Azure { get; set; } -/// The input-required variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepInputRequired : AuthLoginStep -{ - /// - [JsonIgnore] - public override string Kind => "input-required"; + /// Base URL for provider API requests. + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = string.Empty; - /// Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. a GitHub Enterprise Cloud host, *.ghe.com). - [JsonPropertyName("prompt")] - public required string Prompt { get; set; } + /// Static bearer token used to authenticate provider requests. + [JsonPropertyName("bearerToken")] + public string? BearerToken { get; set; } + + /// Whether the host supplies bearer tokens dynamically. + [JsonPropertyName("hasBearerTokenProvider")] + public bool? HasBearerTokenProvider { get; set; } + + /// Additional HTTP headers included with provider requests. + [JsonPropertyName("headers")] + public IDictionary? Headers { get; set; } + + /// The product serving the provider's models, reported in telemetry as `model_provider`. Only affects telemetry. + [JsonPropertyName("modelProvider")] + public ProviderConfigModelProvider? ModelProvider { get; set; } + + /// Unique provider name used to qualify model selection IDs. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Transport used to communicate with the provider. + [JsonPropertyName("transport")] + public ProviderConfigTransport? Transport { get; set; } + + /// Provider protocol family. + [JsonPropertyName("type")] + public ProviderConfigType? Type { get; set; } + + /// Wire API used to communicate with the provider. + [JsonPropertyName("wireApi")] + public ProviderConfigWireApi? WireApi { get; set; } } -/// The awaiting variant of . +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepAwaiting : AuthLoginStep +public sealed class ModelProviderConfigurationPlan { - /// - [JsonIgnore] - public override string Kind => "awaiting"; + /// Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. + [JsonPropertyName("model")] + public ProviderModelConfig Model { get => field ??= new(); set; } + + /// Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. + [JsonPropertyName("modelDisposition")] + public ModelProviderConfigurationDisposition ModelDisposition { get; set; } + + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + [JsonPropertyName("provider")] + public NamedProviderConfig Provider { get => field ??= new(); set; } + + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + [JsonPropertyName("providerDisposition")] + public ModelProviderConfigurationDisposition ProviderDisposition { get; set; } + + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; + + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// The needs-interaction variant of . +/// A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepNeedsInteraction : AuthLoginStep +internal sealed class ModelProviderPrepareConfigurationRequest { - /// - [JsonIgnore] - public override string Kind => "needs-interaction"; + /// The discovered instance that serves the model. + [JsonPropertyName("instance")] + public ModelProviderInstance Instance { get => field ??= new(); set; } + + /// The discovered model to configure. + [JsonPropertyName("model")] + public DiscoveredModel Model { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Terminal result of an interactive login flow. +/// Managed sandbox enforcement state for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthLoginResultDto +public sealed class SandboxEnforcementStatus { - /// Host that was signed in, when completed. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("host")] - public string? Host { get; set; } + /// Whether an enforcement failure has permanently blocked the session. + [JsonPropertyName("blocked")] + public bool Blocked { get; set; } - /// Login that was signed in, when completed. - [JsonPropertyName("login")] - public string? Login { get; set; } + /// The first sandbox enforcement failure that blocked the session. + [JsonPropertyName("reason")] + public string? Reason { get; set; } - /// Terminal disposition of the login. - [JsonPropertyName("status")] - public AuthLoginResultStatus Status { get; set; } + /// Whether the effective managed policy requires an available sandbox backend. + [JsonPropertyName("required")] + public bool Required { get; set; } } -/// The completed variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepCompleted : AuthLoginStep +internal sealed class SessionSandboxGetEnforcementStatusRequest { - /// - [JsonIgnore] - public override string Kind => "completed"; - - /// The terminal login result. - [JsonPropertyName("result")] - public required AuthLoginResultDto Result { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The error variant of . +/// Result of attempting to disable sandboxing for the current session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepError : AuthLoginStep +public sealed class SandboxDisableForSessionResult { - /// - [JsonIgnore] - public override string Kind => "error"; + /// The authoritative sandbox enabled state after the operation. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Human-readable failure message. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Whether this call resolved the pending request and applied the session opt-out. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// A started login flow: its opaque id and first step. +/// Optional informational context describing how and where the permission decision was made. This does not affect permission behavior. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthLoginBegun +public sealed class PermissionDecisionContext { - /// Opaque flow id used to advance or cancel this login. - [JsonPropertyName("flowId")] - public string FlowId { get; set; } = string.Empty; + /// Disposition of the permission request as observed by the responding client. + [JsonPropertyName("outcome")] + public PermissionDecisionOutcome Outcome { get; set; } - /// The first step of the flow. - [JsonPropertyName("step")] - public AuthLoginStep Step { get => field ??= new(); 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; } + + /// Client surface that submitted the response. + [JsonPropertyName("surface")] + public PermissionDecisionSurface Surface { get; set; } } -/// Begin an interactive login flow for a provider kind. Dispatch is kind-only. +/// Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AuthLoginBeginRequest +internal sealed class SandboxDisableForSessionRequest { - /// The provider kind to sign in with. - [JsonPropertyName("kind")] - public LoginProviderKind Kind { get; set; } + /// Optional attribution for the permission decision. + [JsonPropertyName("decisionContext")] + public PermissionDecisionContext? DecisionContext { get; set; } + + /// Identifier of the exact pending sandbox-bypass permission request that authorized the session opt-out. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Advance an in-flight login flow, optionally fulfilling an input-required step. +/// Result of accepting a sandbox path grant. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AuthLoginAdvanceRequest +public sealed class SandboxGrantPathForRequestResult { - /// Opaque flow id from begin. - [JsonPropertyName("flowId")] - public string FlowId { get; set; } = string.Empty; - - /// Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. - [JsonPropertyName("input")] - public string? Input { get; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether this call resolved the pending request and added the path to the session's sandbox policy. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Cancel an in-flight login flow. +/// Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AuthLoginCancelRequest +internal sealed class SandboxGrantPathForRequestRequest { - /// Opaque flow id from begin. - [JsonPropertyName("flowId")] - public string FlowId { get; set; } = string.Empty; + /// Optional attribution for the permission decision. + [JsonPropertyName("decisionContext")] + public PermissionDecisionContext? DecisionContext { get; set; } + + /// Identifier of the exact pending sandbox escalation permission request whose sandboxPathGrant to accept. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A file included in the session debug bundle. +/// Authentication status and account metadata for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsCollectedEntry +public sealed class SessionAuthStatus { - /// Relative path of the file in the staged bundle/archive. - [JsonPropertyName("bundlePath")] - public string BundlePath { get; set; } = string.Empty; + /// Authentication type. + [JsonPropertyName("authType")] + public AuthInfoType? AuthType { get; set; } - /// Redacted output size in bytes. - [JsonPropertyName("sizeBytes")] - public long SizeBytes { get; set; } + /// Copilot plan tier (e.g., individual_pro, business). + [JsonPropertyName("copilotPlan")] + public string? CopilotPlan { get; set; } - /// Source category for this entry. - [JsonPropertyName("source")] - public DebugCollectLogsSource Source { get; set; } -} + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string? Host { get; set; } -/// An optional debug bundle entry that could not be included. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsSkippedEntry -{ - /// Relative path requested for this bundle entry. - [JsonPropertyName("bundlePath")] - public string BundlePath { get; set; } = string.Empty; + /// Whether the session has resolved authentication. + [JsonPropertyName("isAuthenticated")] + public bool IsAuthenticated { get; set; } - /// Server-local source path that could not be read. - [JsonPropertyName("path")] - public string? Path { get; set; } + /// Authenticated login/username, if available. + [JsonPropertyName("login")] + public string? Login { get; set; } - /// Reason the entry was skipped. - [JsonPropertyName("reason")] - public string Reason { get; set; } = string.Empty; + /// Human-readable authentication status description. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } } -/// Result of collecting a session debug bundle. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsResult +internal sealed class SessionGitHubAuthGetStatusRequest { - /// Files included in the bundle. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } - - /// Destination kind that was written. - [JsonPropertyName("kind")] - public DebugCollectLogsResultKind Kind { get; set; } - - /// Actual archive path or staging directory path written. This may differ from the requested path when no-overwrite suffixing or fallback-to-temp-directory was needed. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Optional files or directories that could not be included. - [JsonPropertyName("skippedEntries")] - public IList? SkippedEntries { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// A caller-provided server-local file or directory to include in the debug bundle. +/// Indicates whether the credential update succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsEntry +public sealed class SessionSetCredentialsResult { - /// Relative path to use inside the staged bundle/archive. - [JsonPropertyName("bundlePath")] - public string BundlePath { get; set; } = string.Empty; - - /// Kind of source path to include. - [JsonPropertyName("kind")] - public DebugCollectLogsEntryKind Kind { get; set; } - - /// Server-local source path to read. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// How text content from this entry should be redacted. Defaults to plain-text. With none, no redaction is applied; the caller must ensure any necessary redaction is performed before this call. - [JsonPropertyName("redaction")] - public DebugCollectLogsRedaction? Redaction { get; set; } + /// Whether the session ended up with a populated `copilotUser` for the installed credentials. `true` when the supplied credential already carried `copilotUser` or it was successfully re-resolved server-side. `false` when the credential is installed without `copilotUser` — either re-resolution failed, or the variant cannot be re-resolved from the credential alone (only the raw-token variants `token`, `env`, and `gh-cli` can). In both `false` cases the token swap still applied, but plan/quota/billing metadata is degraded. Present whenever a credential was supplied; omitted only when no credential was supplied (no-op call). + [JsonPropertyName("copilotUserResolved")] + public bool? CopilotUserResolved { get; set; } - /// When true, collection fails if this entry cannot be read. Defaults to false, which records the entry in `skippedEntries`. - [JsonPropertyName("required")] - public bool? Required { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Destination for the session debug bundle. -/// Polymorphic base type discriminated by kind. +/// 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(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", + TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] -[JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] -public partial class DebugCollectLogsDestination +[JsonDerivedType(typeof(SettableAuthInfoHmac), "hmac")] +[JsonDerivedType(typeof(SettableAuthInfoEnv), "env")] +[JsonDerivedType(typeof(SettableAuthInfoToken), "token")] +[JsonDerivedType(typeof(SettableAuthInfoCopilotApiToken), "copilot-api-token")] +[JsonDerivedType(typeof(SettableAuthInfoUser), "user")] +[JsonDerivedType(typeof(SettableAuthInfoAccount), "account")] +[JsonDerivedType(typeof(SettableAuthInfoGhCli), "gh-cli")] +[JsonDerivedType(typeof(SettableAuthInfoApiKey), "api-key")] +public partial class SettableAuthInfo { /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// The archive variant of . +/// Authentication-info input variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret. +/// The hmac variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class DebugCollectLogsDestinationArchive : DebugCollectLogsDestination +public partial class SettableAuthInfoHmac : SettableAuthInfo { /// [JsonIgnore] - public override string Kind => "archive"; + public override string Type => "hmac"; - /// When true, create the archive atomically without overwriting an existing file by appending ` (N)` before the extension as needed. Defaults to false. + /// 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("noOverwrite")] - public bool? NoOverwrite { get; set; } + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } - /// Absolute or server-relative path for the .tgz archive to create. - [JsonPropertyName("outputPath")] - public required string OutputPath { 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; } } -/// The directory variant of . +/// 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(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class DebugCollectLogsDestinationDirectory : DebugCollectLogsDestination +public partial class SettableAuthInfoEnv : SettableAuthInfo { /// [JsonIgnore] - public override string Kind => "directory"; + public override string Type => "env"; - /// Directory where files should be staged. The directory is created if needed. - [JsonPropertyName("outputDirectory")] - public required string OutputDirectory { 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; } -/// Built-in session diagnostics to include in the bundle. Omitted fields default to true. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsInclude -{ - /// Server-local path to the current process log. When set, it is included as `process.log` and its directory is searched for prior logs from the same session. - [JsonPropertyName("currentProcessLogPath")] - public string? CurrentProcessLogPath { get; set; } + /// Name of the environment variable the token was sourced from. + [JsonPropertyName("envVar")] + public required string EnvVar { get; set; } - /// Include the session event log (`events.jsonl`). Defaults to true. - [JsonPropertyName("events")] - public bool? Events { get; set; } + /// Authentication host (e.g. https://github.com or a GHES host). + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Server-local path to the session's events.jsonl file. Internal callers normally omit this and let the runtime derive it from the session. - [JsonPropertyName("eventsPath")] - public string? EventsPath { 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; } - /// Maximum number of previous process logs to include. Defaults to 5. - [JsonPropertyName("previousProcessLogLimit")] - public long? PreviousProcessLogLimit { get; set; } + /// The token value itself. Treat as a secret. + [JsonPropertyName("token")] + public required string Token { get; set; } +} - /// Server-local process log directory to search when `currentProcessLogPath` is unavailable, useful for collecting logs for inactive sessions. - [JsonPropertyName("processLogDirectory")] - public string? ProcessLogDirectory { get; set; } +/// Token authentication accepted by session.gitHubAuth.setCredentials. +/// The token variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SettableAuthInfoToken : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "token"; - /// Include process logs for the session. Defaults to true. - [JsonPropertyName("processLogs")] - public bool? ProcessLogs { 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; } - /// Include interactive shell logs written under the session's `shell-logs` directory. Defaults to true. - [JsonPropertyName("shellLogs")] - public bool? ShellLogs { 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; } } -/// Options for collecting a session debug bundle with configurable redaction. +/// Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host. +/// The copilot-api-token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DebugCollectLogsRequest +public partial class SettableAuthInfoCopilotApiToken : SettableAuthInfo { - /// Caller-provided server-local files or directories to include in addition to the runtime's built-in session diagnostics. This lets host applications add their own diagnostics without changing the API shape. - [JsonPropertyName("additionalEntries")] - public IList? AdditionalEntries { get; set; } - - /// Where the bundle should be written. Use `archive` to produce a .tgz, or `directory` to stage files for caller-managed upload/post-processing. - [JsonPropertyName("destination")] - public DebugCollectLogsDestination Destination { get => field ??= new(); set; } + /// + [JsonIgnore] + public override string Type => "copilot-api-token"; - /// Which built-in session diagnostics to include. Omitted fields default to true. - [JsonPropertyName("include")] - public DebugCollectLogsInclude? Include { 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Authentication host (always the public GitHub host). + [JsonPropertyName("host")] + public required string Host { get; set; } } -/// Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. +/// Authentication-info variant for OAuth user auth, with host and login; the token remains in the runtime secret store. +/// The user variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasAction +public partial class SettableAuthInfoUser : SettableAuthInfo { - /// Description of the action. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// + [JsonIgnore] + public override string Type => "user"; - /// JSON Schema for the action input. - [JsonPropertyName("inputSchema")] - public JsonElement? InputSchema { 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; } - /// Action name exposed by the canvas provider. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// OAuth user login. + [JsonPropertyName("login")] + public required string Login { get; set; } } -/// Canvas available in the current session. +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// The account variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiscoveredCanvas +public partial class SettableAuthInfoAccount : SettableAuthInfo { - /// Actions the agent or host may invoke on an open instance. - [JsonPropertyName("actions")] - public IList? Actions { get; set; } + /// + [JsonIgnore] + public override string Type => "account"; - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; + /// Host coordinate owned by the account's model provider. + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Short, single-sentence description shown to the agent in canvas catalogs. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Login identifying the provider-owned account. + [JsonPropertyName("login")] + public required string Login { get; set; } +} - /// Human-readable canvas name. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; +/// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. +/// The gh-cli variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SettableAuthInfoGhCli : SettableAuthInfo +{ + /// + [JsonIgnore] + public override string Type => "gh-cli"; - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// 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; } - /// Owning extension display name, when available. - [JsonPropertyName("extensionName")] - public string? ExtensionName { get; set; } + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Host-local PNG path for the canvas icon, when supplied. - [JsonPropertyName("icon")] - public string? Icon { get; set; } + /// User login as reported by `gh auth status`. + [JsonPropertyName("login")] + public required string Login { get; set; } - /// JSON Schema for canvas open input. - [JsonPropertyName("inputSchema")] - public JsonElement? InputSchema { get; set; } + /// The token returned by `gh auth token`. Treat as a secret. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// Declared canvases available in this session. +/// 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(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasList +public partial class SettableAuthInfoApiKey : SettableAuthInfo { - /// Declared canvases available in this session. - [JsonPropertyName("canvases")] - public IList Canvases { get => field ??= []; set; } + /// + [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; } } -/// Identifies the target session. +/// New auth credentials to install on the session. Omit to leave credentials unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCanvasListRequest +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 SettableAuthInfo? Credentials { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Open canvas instance snapshot. +/// Credential-free authentication identity safe to expose to hosts and user interfaces. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OpenCanvasInstance +public sealed class AuthIdentity { - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; - - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; - - /// Owning extension display name, when available. - [JsonPropertyName("extensionName")] - public string? ExtensionName { get; set; } - - /// Host-local PNG path for the canvas icon, when supplied. - [JsonPropertyName("icon")] - public string? Icon { get; set; } + /// Snapshot of the authenticated user's Copilot subscription info, if known. + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } - /// Input supplied when the instance was opened. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// Name of the environment variable that supplied the credential, when applicable. + [JsonPropertyName("envVar")] + public string? EnvVar { get; set; } - /// Stable caller-supplied canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Authentication host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Provider-supplied status text. - [JsonPropertyName("status")] - public string? Status { get; set; } + /// Authenticated login, when available. + [JsonPropertyName("login")] + public string? Login { get; set; } - /// Rendered title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. + [JsonPropertyName("registrationId")] + public string? RegistrationId { get; set; } - /// URL for web-rendered canvases. - [JsonPropertyName("url")] - public string? Url { get; set; } + /// Authentication type. + [JsonPropertyName("type")] + public AuthInfoType Type { get; set; } } -/// Live open-canvas snapshot. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasListOpenResult +internal sealed class SessionGitHubAuthGetCurrentAuthInfoRequest { - /// Currently open canvas instances. - [JsonPropertyName("openCanvases")] - public IList OpenCanvases { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCanvasListOpenRequest +internal sealed class SessionGitHubAuthGetAllAuthAvailableRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Canvas open parameters. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasOpenRequest +internal sealed class SessionGitHubAuthRefreshCopilotUserRequest { - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. - [JsonPropertyName("extensionId")] - public string? ExtensionId { get; set; } +/// Internal GitHub login parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionAuthLoginRequest +{ + /// GitHub host URL. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Canvas open input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// GitHub login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Caller-supplied stable instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Whether to persist the token after login. + [JsonPropertyName("persist")] + public bool? Persist { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// GitHub authentication token. + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; } -/// Canvas close parameters. +/// Parameters for switching the session's active authentication. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasCloseRequest +internal sealed class SessionAuthSwitchRequest { - /// Open canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Authentication information to activate. + [JsonPropertyName("authInfo")] + public AuthInfo AuthInfo { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; -} -/// Canvas action invocation result. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasActionInvokeResult -{ - /// Provider-supplied action result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Optional token paired with the authentication information. + [JsonPropertyName("token")] + public string? Token { get; set; } } -/// Canvas action invocation parameters. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasActionInvokeRequest +internal sealed class SessionGitHubAuthLogoutRequest { - /// Action name to invoke. - [JsonPropertyName("actionName")] - public string ActionName { get; set; } = string.Empty; - - /// Action input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } - - /// Open canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Internal canvas provider registration parameters. +/// Parameters identifying a GitHub authentication to log out. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasProviderRegisterRequest +internal sealed class SessionAuthLogoutUserRequest { - /// Canvas contributions supplied by the provider. - [JsonPropertyName("canvases")] - public IList Canvases { get => field ??= []; set; } - - /// Connection identifier for callback routing. - [JsonPropertyName("connectionId")] - public string ConnectionId { get; set; } = string.Empty; - - /// Provider metadata supplied by the host. - [JsonPropertyName("info")] - public JsonElement Info { get; set; } + /// Authentication information to log out. + [JsonPropertyName("authInfo")] + public AuthInfo AuthInfo { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Internal canvas provider unregistration parameters. +/// Validation error from an authentication attempt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasProviderUnregisterRequest +public sealed class AuthValidationError { - /// Connection identifier to unregister. - [JsonPropertyName("connectionId")] - public string ConnectionId { get; set; } = string.Empty; + /// Optional message returned by GitHub. + [JsonPropertyName("githubMessage")] + public string? GitHubMessage { get; set; } + + /// Authentication validation error message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionGitHubAuthLastAuthErrorsRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Machine-readable workflow run failure. -/// Polymorphic base type discriminated by type. +/// The enumerated collection, keyed by the same selector as the query. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", + TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowLimitReached), "workflow_limit_reached")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowResumeDeclined), "workflow_resume_declined")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowDurableFailure), "workflow_durable_failure")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowAccountingIncomplete), "workflow_accounting_incomplete")] -[JsonDerivedType(typeof(WorkflowRunFailureWorkflowProviderDisconnected), "workflow_provider_disconnected")] -public partial class WorkflowRunFailure +[JsonDerivedType(typeof(AuthEnumerateValueAccounts), "accounts")] +[JsonDerivedType(typeof(AuthEnumerateValueProviders), "providers")] +public partial class AuthEnumerateValue { /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// The workflow_limit_reached variant of . +/// One signed-in account in the roster forest. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowLimitReached : WorkflowRunFailure +public sealed class AccountStatus { - /// - [JsonIgnore] - public override string Type => "workflow_limit_reached"; + /// Whether this is the active account. + [JsonPropertyName("active")] + public bool Active { get; set; } - /// Resource ceiling that stopped the run. - [JsonPropertyName("kind")] - public required WorkflowRunFailureKind Kind { get; set; } + /// Opaque id of the account this one was derived from (e.g. an EMU account's base Entra identity); absent for a root account. Matches the base identity account's selectionId, forming the derivation edge. + [JsonPropertyName("derivedFrom")] + public string? DerivedFrom { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Suggested larger ceiling when the runtime can derive one safely. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("suggestedValue")] - public double? SuggestedValue { get; set; } + /// The provider kind of this account. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } - /// Approved effective ceiling that was reached. - [JsonPropertyName("value")] - public required double Value { get; set; } + /// Authenticated login/username. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Opaque selection id used to switch to, or log out, this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; } -/// The workflow_resume_declined variant of . +/// The accounts variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowResumeDeclined : WorkflowRunFailure +public partial class AuthEnumerateValueAccounts : AuthEnumerateValue { /// [JsonIgnore] - public override string Type => "workflow_resume_declined"; - - /// Human-readable reason the resume did not proceed. - [JsonPropertyName("reason")] - public required string Reason { get; set; } + public override string Kind => "accounts"; - /// Workflow run identifier whose changed limits were declined. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// The signed-in account forest; empty when not logged in. + [JsonPropertyName("items")] + public required IList Items { get; set; } } -/// The workflow_durable_failure variant of . +/// A provider offered for interactive login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowDurableFailure : WorkflowRunFailure +public sealed class ProviderDescriptor { - /// - [JsonIgnore] - public override string Type => "workflow_durable_failure"; - - /// Stable failure code. - [JsonPropertyName("code")] - public required string Code { get; set; } + /// Whether this provider is currently available to sign in with. + [JsonPropertyName("available")] + public bool Available { get; set; } - /// Execution-critical durable operation that failed. - [JsonPropertyName("operation")] - public required WorkflowDurableOperation Operation { get; set; } + /// The neutral provider kind. + [JsonPropertyName("kind")] + public LoginProviderKind Kind { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Human-readable menu label, owned by the runtime so every consumer renders identical text. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; } -/// The run stopped because its usage accounting could not be completed. -/// The workflow_accounting_incomplete variant of . +/// The providers variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowAccountingIncomplete : WorkflowRunFailure +public partial class AuthEnumerateValueProviders : AuthEnumerateValue { /// [JsonIgnore] - public override string Type => "workflow_accounting_incomplete"; + public override string Kind => "providers"; - /// Confirmed usage in nano-AIU, representing the floor of what the run spent. - [JsonPropertyName("drainedNanoAiu")] - public required long DrainedNanoAiu { get; set; } + /// The providers offered for interactive login. + [JsonPropertyName("items")] + public required IList Items { get; set; } +} - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } +/// Selects which accounts collection to enumerate. A no-arg selector is the empty-payload variant. +/// Polymorphic base type discriminated by kind. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthEnumerateQueryAccounts), "accounts")] +[JsonDerivedType(typeof(AuthEnumerateQueryProviders), "providers")] +public partial class AuthEnumerateQuery +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// The extension that owns the workflow disconnected while the run was executing, so the host halted it. The run's journaled subagent results are preserved so a resume can reuse them. -/// The workflow_provider_disconnected variant of . + +/// The accounts variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowProviderDisconnected : WorkflowRunFailure +public partial class AuthEnumerateQueryAccounts : AuthEnumerateQuery { /// [JsonIgnore] - public override string Type => "workflow_provider_disconnected"; - - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + public override string Kind => "accounts"; } -/// Durable metadata describing who initiated a workflow pause. -/// Polymorphic base type discriminated by type. +/// The providers variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(WorkflowPauseInfoUser), "user")] -[JsonDerivedType(typeof(WorkflowPauseInfoCheckpoint), "checkpoint")] -public partial class WorkflowPauseInfo +public partial class AuthEnumerateQueryProviders : AuthEnumerateQuery { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "providers"; + + /// Whether an interactive Entra broker is available on the host; gates Entra availability in the returned list. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("brokerAvailable")] + public bool? BrokerAvailable { get; set; } } +/// Enumerate request carrying the typed collection query. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AccountsEnumerateRequest +{ + /// Which typed accounts collection to enumerate. + [JsonPropertyName("query")] + public AuthEnumerateQuery Query { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} -/// The user variant of . +/// The read result, keyed by the same selector as the query. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowPauseInfoUser : WorkflowPauseInfo +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthReadValueActiveAccount), "activeAccount")] +[JsonDerivedType(typeof(AuthReadValueStatus), "status")] +[JsonDerivedType(typeof(AuthReadValueLastErrors), "lastErrors")] +public partial class AuthReadValue { - /// - [JsonIgnore] - public override string Type => "user"; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// The checkpoint variant of . + +/// The activeAccount variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowPauseInfoCheckpoint : WorkflowPauseInfo +public partial class AuthReadValueActiveAccount : AuthReadValue { /// [JsonIgnore] - public override string Type => "checkpoint"; + public override string Kind => "activeAccount"; - /// Stable author-defined checkpoint key that initiated the pause. - [JsonPropertyName("key")] - public required string Key { get; set; } + /// The active account, or absent when not logged in. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("account")] + public AccountStatus? Account { get; set; } + + /// Credential-free identity metadata for the active account, including resolved Copilot user information when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("authInfo")] + public AuthIdentity? AuthInfo { get; set; } } -/// Complete current or terminal workflow run envelope. +/// Neutral authentication status summary. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunResult +public sealed class AuthStatusDto { - /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. - [JsonPropertyName("attempt")] - public long? Attempt { get; set; } - - /// Error message for an errored run. - [JsonPropertyName("error")] - public string? Error { get; set; } - - /// Machine-readable failure details for a halted or errored run. - [JsonPropertyName("failure")] - public WorkflowRunFailure? Failure { get; set; } + /// Number of signed-in accounts in the roster. + [JsonPropertyName("accountCount")] + public long AccountCount { get; set; } - /// Structured pause initiator metadata for a paused attempt. - [JsonPropertyName("pauseInfo")] - public WorkflowPauseInfo? PauseInfo { get; set; } + /// Active account host, if authenticated. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("activeHost")] + public string? ActiveHost { get; set; } - /// Reason for a halted or cancelled run. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Active account login, if authenticated. + [JsonPropertyName("activeLogin")] + public string? ActiveLogin { get; set; } - /// Completed workflow result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Copilot plan tier of the active account, if known. + [JsonPropertyName("copilotPlan")] + public string? CopilotPlan { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Whether the session has resolved authentication. + [JsonPropertyName("isAuthenticated")] + public bool IsAuthenticated { get; set; } +} - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - [JsonPropertyName("snapshot")] - public JsonElement? Snapshot { get; set; } +/// The status variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadValueStatus : AuthReadValue +{ + /// + [JsonIgnore] + public override string Kind => "status"; - /// Current or terminal workflow run status. + /// The neutral authentication status summary. [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + public required AuthStatusDto Status { get; set; } } -/// Wire-only per-invocation workflow resource ceiling overrides. +/// The lastErrors variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunLimits +public partial class AuthReadValueLastErrors : AuthReadValue { - /// Maximum AI credits consumed by workflow subagents and their descendants. The post-paid ceiling is soft: parallel turns can settle beyond it before the run stops. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; set; } - - /// Maximum number of workflow subagents that may run concurrently. - [JsonPropertyName("maxConcurrentSubagents")] - public long? MaxConcurrentSubagents { get; set; } - - /// Maximum total number of workflow subagents that may be admitted. - [JsonPropertyName("maxTotalSubagents")] - public long? MaxTotalSubagents { get; set; } + /// + [JsonIgnore] + public override string Kind => "lastErrors"; - /// Maximum accumulated active-execution time in seconds. Active execution includes the entire extension body, subprocess waits, queued-agent waits, and sleeps; time between resumed attempts is not counted. - [JsonPropertyName("timeoutSeconds")] - public double? TimeoutSeconds { get; set; } + /// Validation errors from the most recent authentication attempt. + [JsonPropertyName("errors")] + public required IList Errors { get; set; } } -/// Options controlling workflow invocation. +/// Selects which typed accounts datum to read. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunOptions +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthReadQueryActiveAccount), "activeAccount")] +[JsonDerivedType(typeof(AuthReadQueryStatus), "status")] +[JsonDerivedType(typeof(AuthReadQueryLastErrors), "lastErrors")] +public partial class AuthReadQuery { - /// Per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } - - /// Whether to emit workflow phase names to the session transcript. - [JsonPropertyName("logPhaseNames")] - public bool? LogPhaseNames { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Whether to notify the originating session when the workflow 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; } +/// The activeAccount variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadQueryActiveAccount : AuthReadQuery +{ + /// + [JsonIgnore] + public override string Kind => "activeAccount"; } -/// Parameters for invoking a registered workflow. +/// The status variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowRunRequest +public partial class AuthReadQueryStatus : AuthReadQuery { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + /// + [JsonIgnore] + public override string Kind => "status"; +} - /// Registered workflow name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// The lastErrors variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadQueryLastErrors : AuthReadQuery +{ + /// + [JsonIgnore] + public override string Kind => "lastErrors"; +} - /// Workflow invocation options. - [JsonPropertyName("options")] - public WorkflowRunOptions? Options { get; set; } +/// Read request carrying the typed datum query. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AccountsGetRequest +{ + /// Which typed accounts datum to read. + [JsonPropertyName("query")] + public AuthReadQuery Query { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Resolved persisted workflow identity and resumed run envelope. +/// Result of a non-interactive accounts mutation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowResumeResult +public sealed class AuthWriteResult { - /// Terminal resumed run envelope. - [JsonPropertyName("run")] - public WorkflowRunResult Run { get => field ??= new(); set; } + /// For a logout, whether other signed-in accounts remain. + [JsonPropertyName("moreUsers")] + public bool? MoreUsers { get; set; } - /// Persisted workflow name resolved for the resumed run. - [JsonPropertyName("workflowName")] - public string WorkflowName { get; set; } = string.Empty; + /// Whether the mutation was applied. + [JsonPropertyName("ok")] + public bool Ok { get; set; } } -/// Parameters for resuming a workflow run from its persisted identity. +/// One non-interactive accounts mutation command (the selector is fused with its typed args). +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowResumeRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthWriteSwitchActive), "switchActive")] +[JsonDerivedType(typeof(AuthWriteLogout), "logout")] +[JsonDerivedType(typeof(AuthWriteSetCredentials), "setCredentials")] +public partial class AuthWrite { - /// Optional per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } - - /// Whether to emit workflow phase names to the session transcript. - [JsonPropertyName("logPhaseNames")] - public bool? LogPhaseNames { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Whether to notify the originating session when the workflow completes. - [JsonPropertyName("notifyOnComplete")] - public bool? NotifyOnComplete { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +/// The switchActive variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthWriteSwitchActive : AuthWrite +{ + /// + [JsonIgnore] + public override string Kind => "switchActive"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Opaque selection id of the account to make active. + [JsonPropertyName("selectionId")] + public required string SelectionId { get; set; } } -/// Options for an internal tool-originated workflow invocation. +/// The logout variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolRunOptions +public partial class AuthWriteLogout : AuthWrite { - /// Per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } + /// + [JsonIgnore] + public override string Kind => "logout"; - /// Run identifier whose journal and progress should seed this resumed run. - [JsonPropertyName("resumeFromRunId")] - public string? ResumeFromRunId { get; set; } + /// Opaque selection id of the account to log out; absent logs out the active account. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("selectionId")] + public string? SelectionId { get; set; } } -/// Internal parameters for invoking a registered workflow from a tool. +/// The setCredentials variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolRunRequest +public partial class AuthWriteSetCredentials : AuthWrite { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + /// + [JsonIgnore] + public override string Kind => "setCredentials"; - /// Registered workflow name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Tool-originated workflow invocation options. - [JsonPropertyName("options")] - public WorkflowToolRunOptions? Options { get; set; } + /// Login/username for the credential. + [JsonPropertyName("login")] + public required string Login { get; set; } + + /// GitHub authentication token to install. + [JsonPropertyName("token")] + public required string Token { get; set; } +} + +/// Mutation request carrying the typed write command. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AccountsSetRequest +{ + /// The non-interactive mutation command to apply. + [JsonPropertyName("command")] + public AuthWrite Command { get => field ??= new(); 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; } +/// One step in an interactive login flow. The consumer acts on the step and calls advance to proceed. Browser-open is encoded as two distinct steps by design: `open-url` is CONSUMER-driven (the provider surfaces the authorize URL and the consumer opens it — github.com/GHEC web), while `needs-interaction` is PROVIDER-driven (the provider opens the browser or broker UI itself and does not surface a URL — Entra). +/// Polymorphic base type discriminated by kind. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthLoginStepOpenUrl), "open-url")] +[JsonDerivedType(typeof(AuthLoginStepInputRequired), "input-required")] +[JsonDerivedType(typeof(AuthLoginStepAwaiting), "awaiting")] +[JsonDerivedType(typeof(AuthLoginStepNeedsInteraction), "needs-interaction")] +[JsonDerivedType(typeof(AuthLoginStepCompleted), "completed")] +[JsonDerivedType(typeof(AuthLoginStepError), "error")] +public partial class AuthLoginStep +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Internal parameters for resuming a workflow run from a tool. + +/// The open-url variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolResumeRequest +public partial class AuthLoginStepOpenUrl : AuthLoginStep { - /// Optional per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } + /// + [JsonIgnore] + public override string Kind => "open-url"; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Authorize URL the consumer should open in a browser (consumer-driven browser-open). + [JsonPropertyName("url")] + public required string Url { get; set; } +} - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// The input-required variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthLoginStepInputRequired : AuthLoginStep +{ + /// + [JsonIgnore] + public override string Kind => "input-required"; - /// Opaque identifier of the originating tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } + /// Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. a GitHub Enterprise Cloud host, *.ghe.com). + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } } -/// Parameters for retrieving a workflow run. +/// The awaiting variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowGetRunRequest +public partial class AuthLoginStepAwaiting : AuthLoginStep { - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "awaiting"; +} - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// The needs-interaction variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthLoginStepNeedsInteraction : AuthLoginStep +{ + /// + [JsonIgnore] + public override string Kind => "needs-interaction"; } -/// Declared or approved workflow resource ceilings. +/// A credential-free account choice after sign-in. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowDeclaredLimits +public sealed class AuthLoginAccount { - /// Maximum AI credits consumed by subagents and descendants. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; set; } + /// Host coordinate owned by the selected account's provider. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Maximum concurrently active subagents. - [JsonPropertyName("maxConcurrentSubagents")] - public long? MaxConcurrentSubagents { get; set; } + /// Provider kind that owns this account choice. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } - /// Maximum total subagents spawned by the run. - [JsonPropertyName("maxTotalSubagents")] - public long? MaxTotalSubagents { get; set; } + /// Human-readable login for the account choice. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Maximum accumulated active execution time in seconds. - [JsonPropertyName("timeoutSeconds")] - public double? TimeoutSeconds { get; set; } + /// Opaque identifier supplied to the next login step to select this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; } -/// Durable workflow resource consumption. +/// Result of an interactive login flow. Pending consent or account selection is not terminal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunConsumed +public sealed class AuthLoginResultDto { - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeMs")] - public long ActiveMs { get; set; } + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + [JsonPropertyName("accounts")] + public IList? Accounts { get; set; } - /// AI usage consumed by the run in nano-AIU. - [JsonPropertyName("nanoAiu")] - public long NanoAiu { get; set; } + /// Host that was signed in, when completed. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string? Host { get; set; } - /// Total subagents spawned by the run. - [JsonPropertyName("subagents")] - public long Subagents { get; set; } + /// Login that was signed in, when completed. + [JsonPropertyName("login")] + public string? Login { get; set; } + + /// Current disposition of the login, including pending user decisions. + [JsonPropertyName("status")] + public AuthLoginResultStatus Status { get; set; } } -/// Current workflow phase identity. +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowCurrentPhase +public partial class AuthLoginStepCompleted : AuthLoginStep { - /// Current phase identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "completed"; - /// Zero-based declared phase ordinal, or null for an undeclared phase. - [JsonPropertyName("ordinal")] - public long? Ordinal { get; set; } + /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. + [JsonPropertyName("result")] + public required AuthLoginResultDto Result { get; set; } } -/// Prompt-safe terminal workflow outcome. +/// The error variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunTerminal +public partial class AuthLoginStepError : AuthLoginStep { - /// Human-readable terminal error. - [JsonPropertyName("error")] - public string? Error { get; set; } - - /// Machine-readable terminal failure. - [JsonPropertyName("failure")] - public WorkflowRunFailure? Failure { get; set; } + /// + [JsonIgnore] + public override string Kind => "error"; - /// Pause initiator metadata, or null when the run did not pause. - [JsonPropertyName("pauseInfo")] - public WorkflowPauseInfo? PauseInfo { get; set; } + /// Human-readable failure message. + [JsonPropertyName("message")] + public required string Message { get; set; } +} - /// Human-readable terminal reason. - [JsonPropertyName("reason")] - public string? Reason { get; set; } +/// A started login flow: its opaque id and first step. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AuthLoginBegun +{ + /// Opaque flow id used to advance or cancel this login. + [JsonPropertyName("flowId")] + public string FlowId { get; set; } = string.Empty; - /// Prompt-safe preview of the completed result. - [JsonPropertyName("resultPreview")] - public string? ResultPreview { get; set; } + /// The first step of the flow. + [JsonPropertyName("step")] + public AuthLoginStep Step { get => field ??= new(); set; } } -/// Durable workflow run summary with read-time live overlays. +/// Begin an interactive login flow for a provider kind. Dispatch is kind-only. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunSummary +internal sealed class AuthLoginBeginRequest { - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { get; set; } + /// The provider kind to sign in with. + [JsonPropertyName("kind")] + public LoginProviderKind Kind { get; set; } - /// Approved effective resource ceilings, or null until approved. - [JsonPropertyName("approved")] - public WorkflowDeclaredLimits? Approved { get; set; } - - /// Whether the durable run state currently passes runtime resume eligibility checks. - [JsonPropertyName("canResume")] - public bool CanResume { get; set; } - - /// Epoch milliseconds when the run completed, or null while nonterminal. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } - - /// Durable resource consumption. - [JsonPropertyName("consumed")] - public WorkflowRunConsumed Consumed { get => field ??= new(); set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Epoch milliseconds when the run was created. - [JsonPropertyName("createdAt")] - public long CreatedAt { get; set; } +/// Advance an in-flight login flow, optionally fulfilling an input-required step. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AuthLoginAdvanceRequest +{ + /// Opaque flow id from begin. + [JsonPropertyName("flowId")] + public string FlowId { get; set; } = string.Empty; - /// Current phase identity, or null before any phase is entered. - [JsonPropertyName("currentPhase")] - public WorkflowCurrentPhase? CurrentPhase { get; set; } + /// Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. + [JsonPropertyName("input")] + public string? Input { get; set; } - /// Resource ceilings declared by the workflow. - [JsonPropertyName("declaredLimits")] - public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Number of phases declared by the workflow. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } +/// Cancel an in-flight login flow. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AuthLoginCancelRequest +{ + /// Opaque flow id from begin. + [JsonPropertyName("flowId")] + public string FlowId { get; set; } = string.Empty; - /// Human-readable workflow description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Number of direct workflow agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } +/// A file included in the session debug bundle. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DebugCollectLogsCollectedEntry +{ + /// Relative path of the file in the staged bundle/archive. + [JsonPropertyName("bundlePath")] + public string BundlePath { get; set; } = string.Empty; - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } + /// Redacted output size in bytes. + [JsonPropertyName("sizeBytes")] + public long SizeBytes { get; set; } - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Source category for this entry. + [JsonPropertyName("source")] + public DebugCollectLogsSource Source { get; set; } +} - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +/// An optional debug bundle entry that could not be included. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DebugCollectLogsSkippedEntry +{ + /// Relative path requested for this bundle entry. + [JsonPropertyName("bundlePath")] + public string BundlePath { get; set; } = string.Empty; - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Server-local source path that could not be read. + [JsonPropertyName("path")] + public string? Path { get; set; } - /// Current workflow run status. - [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + /// Reason the entry was skipped. + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} - /// Terminal run outcome, or null while nonterminal. - [JsonPropertyName("terminal")] - public WorkflowRunTerminal? Terminal { get; set; } +/// Result of collecting a session debug bundle. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DebugCollectLogsResult +{ + /// Files included in the bundle. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } - /// Total direct workflow agents spawned across all attempts. - [JsonPropertyName("totalSpawnedAgentCount")] - public long TotalSpawnedAgentCount { get; set; } + /// Destination kind that was written. + [JsonPropertyName("kind")] + public DebugCollectLogsResultKind Kind { get; set; } - /// Epoch milliseconds when the durable run was last updated. - [JsonPropertyName("updatedAt")] - public long UpdatedAt { get; set; } + /// Actual archive path or staging directory path written. This may differ from the requested path when no-overwrite suffixing or fallback-to-temp-directory was needed. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Registered workflow name. - [JsonPropertyName("workflowName")] - public string WorkflowName { get; set; } = string.Empty; + /// Optional files or directories that could not be included. + [JsonPropertyName("skippedEntries")] + public IList? SkippedEntries { get; set; } } -/// A page of workflow runs in durable creation order. +/// A caller-provided server-local file or directory to include in the debug bundle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowListRunsResult +public sealed class DebugCollectLogsEntry { - /// Whether terminal runs newer than this page exist. - [JsonPropertyName("hasMoreNewer")] - public bool? HasMoreNewer { get; set; } + /// Relative path to use inside the staged bundle/archive. + [JsonPropertyName("bundlePath")] + public string BundlePath { get; set; } = string.Empty; - /// Newest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("newestSeq")] - public long? NewestSeq { get; set; } + /// Kind of source path to include. + [JsonPropertyName("kind")] + public DebugCollectLogsEntryKind Kind { get; set; } - /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("oldestSeq")] - public long? OldestSeq { get; set; } + /// Server-local source path to read. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Number of terminal runs older than this page. - [JsonPropertyName("omittedOlder")] - public long? OmittedOlder { get; set; } + /// How text content from this entry should be redacted. Defaults to plain-text. With none, no redaction is applied; the caller must ensure any necessary redaction is performed before this call. + [JsonPropertyName("redaction")] + public DebugCollectLogsRedaction? Redaction { get; set; } - /// Workflow run summaries in durable creation order. - [JsonPropertyName("runs")] - public IList Runs { get => field ??= []; set; } + /// When true, collection fails if this entry cannot be read. Defaults to false, which records the entry in `skippedEntries`. + [JsonPropertyName("required")] + public bool? Required { get; set; } } -/// Parameters for paging workflow runs. +/// Destination for the session debug bundle. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowListRunsRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] +[JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] +public partial class DebugCollectLogsDestination { - /// Exclusive forward cursor. - [JsonPropertyName("afterSeq")] - public long? AfterSeq { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Exclusive backward cursor. - [JsonPropertyName("beforeSeq")] - public long? BeforeSeq { get; set; } - /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. - [JsonPropertyName("limit")] - public int? Limit { get; set; } +/// The archive variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class DebugCollectLogsDestinationArchive : DebugCollectLogsDestination +{ + /// + [JsonIgnore] + public override string Kind => "archive"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// When true, create the archive atomically without overwriting an existing file by appending ` (N)` before the extension as needed. Defaults to false. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("noOverwrite")] + public bool? NoOverwrite { get; set; } + + /// Absolute or server-relative path for the .tgz archive to create. + [JsonPropertyName("outputPath")] + public required string OutputPath { get; set; } } -/// Prompt-safe durable identity and live status for a direct workflow agent. +/// The directory variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentSummary +public partial class DebugCollectLogsDestinationDirectory : DebugCollectLogsDestination { - /// Accumulated active agent time in milliseconds. - [JsonPropertyName("activeMs")] - public long ActiveMs { get; set; } - - /// Prompt-safe live activity text. - [JsonPropertyName("activity")] - public string? Activity { get; set; } + /// + [JsonIgnore] + public override string Kind => "directory"; - /// Stable direct-agent identifier. - [JsonPropertyName("agentId")] - public string AgentId { get; set; } = string.Empty; + /// Directory where files should be staged. The directory is created if needed. + [JsonPropertyName("outputDirectory")] + public required string OutputDirectory { get; set; } +} - /// Registered agent type. - [JsonPropertyName("agentType")] - public string AgentType { get; set; } = string.Empty; +/// Built-in session diagnostics to include in the bundle. Omitted fields default to true. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DebugCollectLogsInclude +{ + /// Server-local path to the current process log. When set, it is included as `process.log` and its directory is searched for prior logs from the same session. + [JsonPropertyName("currentProcessLogPath")] + public string? CurrentProcessLogPath { get; set; } - /// Epoch milliseconds when the agent completed. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } + /// Include the session event log (`events.jsonl`). Defaults to true. + [JsonPropertyName("events")] + public bool? Events { get; set; } - /// Friendly, non-unique name intended for display. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Server-local path to the session's events.jsonl file. Internal callers normally omit this and let the runtime derive it from the session. + [JsonPropertyName("eventsPath")] + public string? EventsPath { get; set; } - /// Friendly, non-unique name intended for display. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; + /// Maximum number of previous process logs to include. Defaults to 5. + [JsonPropertyName("previousProcessLogLimit")] + public long? PreviousProcessLogLimit { get; set; } - /// Phase identifier active when the agent was launched, or null. - [JsonPropertyName("phaseId")] - public string? PhaseId { get; set; } + /// Server-local process log directory to search when `currentProcessLogPath` is unavailable, useful for collecting logs for inactive sessions. + [JsonPropertyName("processLogDirectory")] + public string? ProcessLogDirectory { get; set; } - /// Model requested when the agent was launched. - [JsonPropertyName("requestedModel")] - public string? RequestedModel { get; set; } + /// Include process logs for the session. Defaults to true. + [JsonPropertyName("processLogs")] + public bool? ProcessLogs { get; set; } - /// Concrete model resolved for the agent. - [JsonPropertyName("resolvedModel")] - public string? ResolvedModel { get; set; } + /// Include interactive shell logs written under the session's `shell-logs` directory. Defaults to true. + [JsonPropertyName("shellLogs")] + public bool? ShellLogs { get; set; } +} - /// Owning workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +/// Options for collecting a session debug bundle with configurable redaction. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class DebugCollectLogsRequest +{ + /// Caller-provided server-local files or directories to include in addition to the runtime's built-in session diagnostics. This lets host applications add their own diagnostics without changing the API shape. + [JsonPropertyName("additionalEntries")] + public IList? AdditionalEntries { get; set; } - /// Epoch milliseconds when the agent started. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Where the bundle should be written. Use `archive` to produce a .tgz, or `directory` to stage files for caller-managed upload/post-processing. + [JsonPropertyName("destination")] + public DebugCollectLogsDestination Destination { get => field ??= new(); set; } - /// Current durable or live agent status. - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; + /// Which built-in session diagnostics to include. Omitted fields default to true. + [JsonPropertyName("include")] + public DebugCollectLogsInclude? Include { get; set; } - /// Tool-call identifier that launched the agent. - [JsonPropertyName("toolCallId")] - public string ToolCallId { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Durable lifecycle and timing for one workflow phase. +/// Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowPhaseObservation +public sealed class CanvasAction { - /// Completed active time accumulated by this phase in milliseconds. - [JsonPropertyName("accumulatedActiveMs")] - public long AccumulatedActiveMs { get; set; } + /// Description of the action. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip timestamp (equal to `startedAt`). - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } + /// JSON Schema for the action input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } - /// Current live active time for this phase in milliseconds. - [JsonPropertyName("currentActiveMs")] - public long CurrentActiveMs { get; set; } + /// Action name exposed by the canvas provider. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} - /// Optional human-readable phase detail. - [JsonPropertyName("detail")] - public string? Detail { get; set; } +/// Canvas available in the current session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class DiscoveredCanvas +{ + /// Actions the agent or host may invoke on an open instance. + [JsonPropertyName("actions")] + public IList? Actions { get; set; } - /// Number of times execution entered this phase. - [JsonPropertyName("entryCount")] - public long EntryCount { get; set; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Phase identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Short, single-sentence description shown to the agent in canvas catalogs. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Most recent run attempt that entered this phase, or `0` if the phase has never been entered. - [JsonPropertyName("lastEnteredRunAttempt")] - public long LastEnteredRunAttempt { get; set; } + /// Human-readable canvas name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Direct agents in this phase that are currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Zero-based declared phase ordinal, or null for an undeclared phase. - [JsonPropertyName("ordinal")] - public long? Ordinal { get; set; } + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } - /// Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip timestamp (equal to `completedAt`). - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } - /// Derived lifecycle state of the phase. - [JsonPropertyName("status")] - public WorkflowPhaseStatus Status { get; set; } + /// JSON Schema for canvas open input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } +} - /// Human-readable phase title. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; +/// Declared canvases available in this session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasList +{ + /// Declared canvases available in this session. + [JsonPropertyName("canvases")] + public IList Canvases { get => field ??= []; set; } +} - /// Total direct agents associated with this phase. - [JsonPropertyName("totalAgentCount")] - public long TotalAgentCount { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionCanvasListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// One durable workflow progress record. +/// Open canvas instance snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowProgressLine +public sealed class OpenCanvasInstance { - /// Resume attempt that emitted this record. - [JsonPropertyName("attempt")] - public long Attempt { get; set; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Progress record kind. - [JsonPropertyName("kind")] - public WorkflowLogLineKind Kind { get; set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Phase active when the record was emitted, or null before any phase. - [JsonPropertyName("phaseId")] - public string? PhaseId { get; set; } + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } - /// Epoch milliseconds when the record was persisted. - [JsonPropertyName("recordedAt")] - public long RecordedAt { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } - /// Global monotonic sequence number within the run. - [JsonPropertyName("seq")] - public long Seq { get; set; } + /// Input supplied when the instance was opened. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Prompt-safe progress text. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// Stable caller-supplied canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Provider-supplied status text. + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// Rendered title. + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// URL for web-rendered canvases. + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// A bidirectional page of workflow progress. +/// Live open-canvas snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowProgressPage +public sealed class CanvasListOpenResult { - /// Whether progress records newer than this page exist. - [JsonPropertyName("hasMoreNewer")] - public bool HasMoreNewer { get; set; } + /// Currently open canvas instances. + [JsonPropertyName("openCanvases")] + public IList OpenCanvases { get => field ??= []; set; } +} - /// Whether progress records older than this page exist. - [JsonPropertyName("hasMoreOlder")] - public bool HasMoreOlder { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionCanvasListOpenRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Newest sequence number in this page, or null when empty. - [JsonPropertyName("newestSeq")] - public long? NewestSeq { get; set; } +/// Canvas open parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CanvasOpenRequest +{ + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Oldest sequence number in this page, or null when empty. - [JsonPropertyName("oldestSeq")] - public long? OldestSeq { get; set; } + /// Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. + [JsonPropertyName("extensionId")] + public string? ExtensionId { get; set; } - /// Progress records in sequence order. - [JsonPropertyName("records")] - public IList Records { get => field ??= []; set; } + /// Canvas open input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Run revision reflected by this page. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Caller-supplied stable instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Full workflow run observability detail. +/// Canvas close parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunDetail +internal sealed class CanvasCloseRequest { - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { get; set; } + /// Open canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; - /// Durable identities and live statuses for direct workflow agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Approved effective resource ceilings, or null until approved. - [JsonPropertyName("approved")] - public WorkflowDeclaredLimits? Approved { get; set; } +/// Canvas action invocation result. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasActionInvokeResult +{ + /// Provider-supplied action result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} - /// Whether the durable run state currently passes runtime resume eligibility checks. - [JsonPropertyName("canResume")] - public bool CanResume { get; set; } +/// Canvas action invocation parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CanvasActionInvokeRequest +{ + /// Action name to invoke. + [JsonPropertyName("actionName")] + public string ActionName { get; set; } = string.Empty; - /// Epoch milliseconds when the run completed, or null while nonterminal. - [JsonPropertyName("completedAt")] - public long? CompletedAt { get; set; } + /// Action input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Durable resource consumption. - [JsonPropertyName("consumed")] - public WorkflowRunConsumed Consumed { get => field ??= new(); set; } + /// Open canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; - /// Epoch milliseconds when the run was created. - [JsonPropertyName("createdAt")] - public long CreatedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Current phase identity, or null before any phase is entered. - [JsonPropertyName("currentPhase")] - public WorkflowCurrentPhase? CurrentPhase { get; set; } +/// Internal canvas provider registration parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CanvasProviderRegisterRequest +{ + /// Canvas contributions supplied by the provider. + [JsonPropertyName("canvases")] + public IList Canvases { get => field ??= []; set; } - /// Resource ceilings declared by the workflow. - [JsonPropertyName("declaredLimits")] - public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } + /// Connection identifier for callback routing. + [JsonPropertyName("connectionId")] + public string ConnectionId { get; set; } = string.Empty; - /// Number of phases declared by the workflow. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } + /// Provider metadata supplied by the host. + [JsonPropertyName("info")] + public JsonElement Info { get; set; } - /// Human-readable workflow description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Number of direct workflow agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } +/// Internal canvas provider unregistration parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CanvasProviderUnregisterRequest +{ + /// Connection identifier to unregister. + [JsonPropertyName("connectionId")] + public string ConnectionId { get; set; } = string.Empty; - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Lifecycle and timing observations for each workflow phase. - [JsonPropertyName("phases")] - public IList Phases { get => field ??= []; set; } +/// Machine-readable workflow run failure. +/// Polymorphic base type discriminated by type. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(WorkflowRunFailureWorkflowLimitReached), "workflow_limit_reached")] +[JsonDerivedType(typeof(WorkflowRunFailureWorkflowResumeDeclined), "workflow_resume_declined")] +[JsonDerivedType(typeof(WorkflowRunFailureWorkflowDurableFailure), "workflow_durable_failure")] +[JsonDerivedType(typeof(WorkflowRunFailureWorkflowAccountingIncomplete), "workflow_accounting_incomplete")] +[JsonDerivedType(typeof(WorkflowRunFailureWorkflowProviderDisconnected), "workflow_provider_disconnected")] +public partial class WorkflowRunFailure +{ + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; +} - /// Bidirectional page of durable workflow progress. - [JsonPropertyName("progress")] - public WorkflowProgressPage Progress { get => field ??= new(); set; } - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } +/// The workflow_limit_reached variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class WorkflowRunFailureWorkflowLimitReached : WorkflowRunFailure +{ + /// + [JsonIgnore] + public override string Type => "workflow_limit_reached"; + + /// Resource ceiling that stopped the run. + [JsonPropertyName("kind")] + public required WorkflowRunFailureKind Kind { get; set; } /// Workflow run identifier. [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + public required string RunId { get; set; } - /// Current workflow run status. - [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + /// Suggested larger ceiling when the runtime can derive one safely. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("suggestedValue")] + public double? SuggestedValue { get; set; } - /// Terminal run outcome, or null while nonterminal. - [JsonPropertyName("terminal")] - public WorkflowRunTerminal? Terminal { get; set; } + /// Approved effective ceiling that was reached. + [JsonPropertyName("value")] + public required double Value { get; set; } +} - /// Total direct workflow agents spawned across all attempts. - [JsonPropertyName("totalSpawnedAgentCount")] - public long TotalSpawnedAgentCount { get; set; } +/// The workflow_resume_declined variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class WorkflowRunFailureWorkflowResumeDeclined : WorkflowRunFailure +{ + /// + [JsonIgnore] + public override string Type => "workflow_resume_declined"; - /// Epoch milliseconds when the durable run was last updated. - [JsonPropertyName("updatedAt")] - public long UpdatedAt { get; set; } + /// Human-readable reason the resume did not proceed. + [JsonPropertyName("reason")] + public required string Reason { get; set; } - /// Registered workflow name. - [JsonPropertyName("workflowName")] - public string WorkflowName { get; set; } = string.Empty; + /// Workflow run identifier whose changed limits were declined. + [JsonPropertyName("runId")] + public required string RunId { get; set; } } -/// Parameters for paging workflow progress. +/// The workflow_durable_failure variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowGetRunProgressRequest +public partial class WorkflowRunFailureWorkflowDurableFailure : WorkflowRunFailure { - /// Exclusive forward cursor. - [JsonPropertyName("afterSeq")] - public long? AfterSeq { get; set; } - - /// Exclusive backward cursor. - [JsonPropertyName("beforeSeq")] - public long? BeforeSeq { get; set; } + /// + [JsonIgnore] + public override string Type => "workflow_durable_failure"; - /// Maximum records to return. Defaults to 200 and is capped at 500. - [JsonPropertyName("limit")] - public int? Limit { get; set; } + /// Stable failure code. + [JsonPropertyName("code")] + public required string Code { get; set; } - /// Optional phase identifier used to scope records and cursors. - [JsonPropertyName("phaseId")] - public string? PhaseId { get; set; } + /// Execution-critical durable operation that failed. + [JsonPropertyName("operation")] + public required WorkflowDurableOperation Operation { get; set; } /// Workflow run identifier. [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + public required string RunId { get; set; } } -/// Parameters for cancelling a workflow run. +/// The run stopped because its usage accounting could not be completed. +/// The workflow_accounting_incomplete variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowCancelRequest +public partial class WorkflowRunFailureWorkflowAccountingIncomplete : WorkflowRunFailure { + /// + [JsonIgnore] + public override string Type => "workflow_accounting_incomplete"; + + /// Confirmed usage in nano-AIU, representing the floor of what the run spent. + [JsonPropertyName("drainedNanoAiu")] + public required long DrainedNanoAiu { get; set; } + /// Workflow run identifier. [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + public required string RunId { get; set; } } -/// Parameters for pausing a running workflow. +/// The extension that owns the workflow disconnected while the run was executing, so the host halted it. The run's journaled subagent results are preserved so a resume can reuse them. +/// The workflow_provider_disconnected variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowPauseRequest +public partial class WorkflowRunFailureWorkflowProviderDisconnected : WorkflowRunFailure { + /// + [JsonIgnore] + public override string Type => "workflow_provider_disconnected"; + /// Workflow run identifier. [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + public required string RunId { get; set; } } -/// RPC data type for SessionWorkflowPauseAtCheckpoint operations. +/// Durable metadata describing who initiated a workflow pause. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkflowPauseAtCheckpointResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(WorkflowPauseInfoUser), "user")] +[JsonDerivedType(typeof(WorkflowPauseInfoCheckpoint), "checkpoint")] +public partial class WorkflowPauseInfo { - /// Whether this execution attempt must pause or may continue. - [JsonPropertyName("action")] - public WorkflowPauseCheckpointAction Action { get; set; } + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Parameters for an owned durable pause checkpoint. + +/// The user variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowPauseCheckpointRequest +public partial class WorkflowPauseInfoUser : WorkflowPauseInfo { - /// Opaque token identifying the execution attempt that reached the checkpoint. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; - - /// Stable author-defined checkpoint key. - [JsonPropertyName("key")] - public string Key { get; set; } = string.Empty; - - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "user"; } -/// Acknowledgement that a workflow request was accepted. +/// The checkpoint variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAckResult +public partial class WorkflowPauseInfoCheckpoint : WorkflowPauseInfo { + /// + [JsonIgnore] + public override string Type => "checkpoint"; + + /// Stable author-defined checkpoint key that initiated the pause. + [JsonPropertyName("key")] + public required string Key { get; set; } } -/// One ordered workflow progress line. +/// Complete current or terminal workflow run envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowLogLine +public sealed class WorkflowRunResult { - /// Progress line kind. - [JsonPropertyName("kind")] - public WorkflowLogLineKind Kind { get; set; } + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + [JsonPropertyName("attempt")] + public long? Attempt { get; set; } - /// Monotonic sequence number within the workflow run. - [JsonPropertyName("seq")] - public long Seq { get; set; } + /// Error message for an errored run. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Progress text. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; -} + /// Machine-readable failure details for a halted or errored run. + [JsonPropertyName("failure")] + public WorkflowRunFailure? Failure { get; set; } -/// Parameters for recording workflow progress. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowLogRequest -{ - /// Opaque token identifying the current workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Structured pause initiator metadata for a paused attempt. + [JsonPropertyName("pauseInfo")] + public WorkflowPauseInfo? PauseInfo { get; set; } - /// Ordered progress lines to append. - [JsonPropertyName("lines")] - public IList Lines { get => field ??= []; set; } + /// Reason for a halted or cancelled run. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Completed workflow result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } /// Workflow run identifier. [JsonPropertyName("runId")] public string RunId { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + [JsonPropertyName("snapshot")] + public JsonElement? Snapshot { get; set; } + + /// Current or terminal workflow run status. + [JsonPropertyName("status")] + public WorkflowRunStatus Status { get; set; } } -/// Result of one workflow-scoped subagent call. +/// Wire-only per-invocation workflow resource ceiling overrides. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentResult +public sealed class WorkflowRunLimits { - /// Agent result, omitted when the agent produced no result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Maximum AI credits consumed by workflow subagents and their descendants. The post-paid ceiling is soft: parallel turns can settle beyond it before the run stops. + [JsonPropertyName("maxAiCredits")] + public double? MaxAiCredits { get; set; } + + /// Maximum number of workflow subagents that may run concurrently. + [JsonPropertyName("maxConcurrentSubagents")] + public long? MaxConcurrentSubagents { get; set; } + + /// Maximum total number of workflow subagents that may be admitted. + [JsonPropertyName("maxTotalSubagents")] + public long? MaxTotalSubagents { get; set; } + + /// Maximum accumulated active-execution time in seconds. Active execution includes the entire extension body, subprocess waits, queued-agent waits, and sleeps; time between resumed attempts is not counted. + [JsonPropertyName("timeoutSeconds")] + public double? TimeoutSeconds { get; set; } } -/// Options for one workflow-scoped subagent call. +/// Options controlling workflow invocation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentOptions +public sealed class WorkflowRunOptions { - /// Optional built-in or custom agent name whose definition configures the subagent. - [JsonPropertyName("agent")] - public string? Agent { get; set; } - - /// Optional context tier override for the subagent. - [JsonPropertyName("contextTier")] - public ContextTier? ContextTier { get; set; } - - /// Optional label distinguishing otherwise identical memoized agent calls. - [JsonPropertyName("label")] - public string? Label { get; set; } + /// Per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Optional model identifier for the subagent. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Whether to emit workflow phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } - /// Optional reasoning effort override for the subagent. - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + /// Whether to notify the originating session when the workflow completes. + [JsonPropertyName("notifyOnComplete")] + public bool? NotifyOnComplete { get; set; } - /// Optional JSON Schema for structured agent output. - [JsonPropertyName("schema")] - public JsonElement? Schema { get; set; } + /// Run identifier whose journal and progress should seed this resumed run. + [JsonPropertyName("resumeFromRunId")] + public string? ResumeFromRunId { get; set; } } -/// Parameters for one workflow-scoped subagent call. +/// Parameters for invoking a registered workflow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowAgentRequest +internal sealed class WorkflowRunRequest { - /// Opaque token identifying the current workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } - /// Subagent execution options. - [JsonPropertyName("opts")] - public WorkflowAgentOptions Opts { get => field ??= new(); set; } + /// Registered workflow name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Prompt to send to the subagent. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Workflow invocation options. + [JsonPropertyName("options")] + public WorkflowRunOptions? Options { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Workflow run identifier that owns the subagent. - [JsonPropertyName("workflowRunId")] - public string WorkflowRunId { get; set; } = string.Empty; } -/// Result of reading a workflow journal entry. +/// Resolved persisted workflow identity and resumed run envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowJournalGetResult +public sealed class WorkflowResumeResult { - /// Whether the journal contained the requested key. - [JsonPropertyName("hit")] - public bool Hit { get; set; } + /// Terminal resumed run envelope. + [JsonPropertyName("run")] + public WorkflowRunResult Run { get => field ??= new(); set; } - /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. - [JsonPropertyName("resultJson")] - public JsonElement? ResultJson { get; set; } + /// Persisted workflow name resolved for the resumed run. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; } -/// Parameters for reading a workflow journal entry. +/// Parameters for resuming a workflow run from its persisted identity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowJournalGetRequest +internal sealed class WorkflowResumeRequest { - /// Opaque token identifying the current workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Optional per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Namespaced journal key. - [JsonPropertyName("key")] - public string Key { get; set; } = string.Empty; + /// Whether to emit workflow phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } + + /// Whether to notify the originating session when the workflow completes. + [JsonPropertyName("notifyOnComplete")] + public bool? NotifyOnComplete { get; set; } /// Workflow run identifier. [JsonPropertyName("runId")] @@ -11611,4099 +12664,3877 @@ internal sealed class WorkflowJournalGetRequest public string SessionId { get; set; } = string.Empty; } -/// Parameters for storing a workflow journal entry. +/// Options for an internal tool-originated workflow invocation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowJournalPutRequest +internal sealed class WorkflowToolRunOptions { - /// Opaque token identifying the current workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Namespaced journal key. - [JsonPropertyName("key")] - public string Key { get; set; } = string.Empty; + /// Run identifier whose journal and progress should seed this resumed run. + [JsonPropertyName("resumeFromRunId")] + public string? ResumeFromRunId { get; set; } +} - /// JSON result to memoize. - [JsonPropertyName("resultJson")] - public JsonElement ResultJson { get; set; } +/// Internal parameters for invoking a registered workflow from a tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowToolRunRequest +{ + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Registered workflow name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Tool-originated workflow invocation options. + [JsonPropertyName("options")] + public WorkflowToolRunOptions? 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; } } -/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. +/// Internal parameters for resuming a workflow run from a tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CurrentModel +internal sealed class WorkflowToolResumeRequest { - /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. - [JsonPropertyName("activatingAutoTier")] - public AutoTier? ActivatingAutoTier { get; set; } - - /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. - [JsonPropertyName("autoTier")] - public AutoTier? AutoTier { get; set; } - - /// Context tier for models that support multiple context-window sizes. - [JsonPropertyName("contextTier")] - public ContextTier? ContextTier { get; set; } - - /// Currently active model identifier. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } - - /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. - [JsonPropertyName("pendingAutoTier")] - public AutoTier? PendingAutoTier { get; set; } + /// Optional per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } -} + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModelGetCurrentRequest -{ /// 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; } } -/// RPC data type for ModelSwitchConfirmation operations. +/// Parameters for retrieving a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchConfirmation +internal sealed class WorkflowGetRunRequest { - /// Current conversation token count before switching models. - [JsonPropertyName("currentTokens")] - public double CurrentTokens { get; set; } - - /// Target model token limit used by the compaction preflight. - [JsonPropertyName("targetLimit")] - public double TargetLimit { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Display name of the model that requires compaction confirmation. - [JsonPropertyName("targetModelDisplayName")] - public string TargetModelDisplayName { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The model identifier active on the session after the switch. +/// Declared or approved workflow resource ceilings. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchToResult +public sealed class WorkflowDeclaredLimits { - /// Compaction confirmation projection when status is confirmation_required. - [JsonPropertyName("confirmation")] - public ModelSwitchConfirmation? Confirmation { get; set; } - - /// True when the switch was deferred (enqueued as a cancellable `/model` command) because a turn was active or another model change was already queued, rather than applied immediately. When true, the session's live model is unchanged until the queued change drains. - [JsonPropertyName("deferred")] - public bool? Deferred { get; set; } - - /// Deprecation warnings associated with the selected model or options. - [JsonPropertyName("deprecationWarnings")] - public IList? DeprecationWarnings { get; set; } - - /// User-facing outcome message for the model switch. - [JsonPropertyName("message")] - public string? Message { get; set; } - - /// Currently active model identifier after the switch. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } - - /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. - [JsonPropertyName("modelState")] - public CurrentModel? ModelState { get; set; } - - /// Persistence failure encountered after applying the model switch. - [JsonPropertyName("persistenceError")] - public string? PersistenceError { get; set; } + /// Maximum AI credits consumed by subagents and descendants. + [JsonPropertyName("maxAiCredits")] + public double? MaxAiCredits { get; set; } - /// Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. - [JsonPropertyName("queueId")] - public string? QueueId { get; set; } + /// Maximum concurrently active subagents. + [JsonPropertyName("maxConcurrentSubagents")] + public long? MaxConcurrentSubagents { get; set; } - /// Lifecycle result for the requested switch. - [JsonPropertyName("status")] - public string? Status { get; set; } + /// Maximum total subagents spawned by the run. + [JsonPropertyName("maxTotalSubagents")] + public long? MaxTotalSubagents { get; set; } - /// User-facing warning produced while applying the model switch. - [JsonPropertyName("warning")] - public string? Warning { get; set; } + /// Maximum accumulated active execution time in seconds. + [JsonPropertyName("timeoutSeconds")] + public double? TimeoutSeconds { get; set; } } -/// Vision-specific limits. +/// Durable workflow resource consumption. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideLimitsVision +public sealed class WorkflowRunConsumed { - /// Maximum image size in bytes. - [JsonPropertyName("max_prompt_image_size")] - public long? MaxPromptImageSize { get; set; } + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeMs")] + public long ActiveMs { get; set; } - /// Maximum number of images per prompt. - [JsonPropertyName("max_prompt_images")] - public long? MaxPromptImages { get; set; } + /// AI usage consumed by the run in nano-AIU. + [JsonPropertyName("nanoAiu")] + public long NanoAiu { get; set; } - /// MIME types the model accepts. - [JsonPropertyName("supported_media_types")] - public IList? SupportedMediaTypes { get; set; } + /// Total subagents spawned by the run. + [JsonPropertyName("subagents")] + public long Subagents { get; set; } } -/// Token limits for prompts, outputs, and context window. +/// Current workflow phase identity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideLimits +public sealed class WorkflowCurrentPhase { - /// Maximum total context window size in tokens. - [JsonPropertyName("max_context_window_tokens")] - public long? MaxContextWindowTokens { get; set; } - - /// Maximum number of output/completion tokens. - [JsonPropertyName("max_output_tokens")] - public long? MaxOutputTokens { get; set; } - - /// Maximum number of prompt/input tokens. - [JsonPropertyName("max_prompt_tokens")] - public long? MaxPromptTokens { get; set; } + /// Current phase identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Vision-specific limits. - [JsonPropertyName("vision")] - public ModelCapabilitiesOverrideLimitsVision? Vision { get; set; } + /// Zero-based declared phase ordinal, or null for an undeclared phase. + [JsonPropertyName("ordinal")] + public long? Ordinal { get; set; } } -/// Feature flags indicating what the model supports. +/// Prompt-safe terminal workflow outcome. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideSupports +public sealed class WorkflowRunTerminal { - /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). - [JsonPropertyName("adaptive_thinking")] - public AdaptiveThinkingSupport? AdaptiveThinking { get; set; } - - /// Whether this model supports reasoning effort configuration. - [JsonPropertyName("reasoningEffort")] - public bool? ReasoningEffort { get; set; } - - /// Whether this model supports canonical tool calling. - [JsonPropertyName("toolCalls")] - public bool? ToolCalls { get; set; } + /// Human-readable terminal error. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Whether this model supports vision/image input. - [JsonPropertyName("vision")] - public bool? Vision { get; set; } -} + /// Machine-readable terminal failure. + [JsonPropertyName("failure")] + public WorkflowRunFailure? Failure { get; set; } -/// Optional capability overrides (vision, tool_calls, reasoning, etc.). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverride -{ - /// Token limits for prompts, outputs, and context window. - [JsonPropertyName("limits")] - public ModelCapabilitiesOverrideLimits? Limits { get; set; } + /// Pause initiator metadata, or null when the run did not pause. + [JsonPropertyName("pauseInfo")] + public WorkflowPauseInfo? PauseInfo { get; set; } - /// Feature flags indicating what the model supports. - [JsonPropertyName("supports")] - public ModelCapabilitiesOverrideSupports? Supports { get; set; } -} + /// Human-readable terminal reason. + [JsonPropertyName("reason")] + public string? Reason { get; set; } -/// Environment variables consulted while resolving model-picker settings. -public sealed class ModelPickerSettingsContextEnvironment -{ + /// Prompt-safe preview of the completed result. + [JsonPropertyName("resultPreview")] + public string? ResultPreview { get; set; } } -/// Filesystem and environment context used to resolve model-picker settings. +/// Durable workflow run summary with read-time live overlays. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelPickerSettingsContext +public sealed class WorkflowRunSummary { - /// Optional Copilot configuration directory containing persisted settings. - [JsonPropertyName("configDir")] - public string? ConfigDir { get; set; } - - /// Environment variables consulted while resolving model-picker settings. - [JsonPropertyName("environment")] - public ModelPickerSettingsContextEnvironment Environment { get => field ??= new(); set; } - - /// User home directory used when resolving persisted settings. - [JsonPropertyName("homeDirectory")] - public string HomeDirectory { get; set; } = string.Empty; -} + /// Epoch milliseconds when the current active segment started, or null while inactive. + [JsonPropertyName("activeSegmentStartedAt")] + public long? ActiveSegmentStartedAt { get; set; } -/// RPC data type for ModelPickerPersistence operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelPickerPersistenceRequest -{ - /// Whether context tier was explicitly selected and should be persisted. - [JsonPropertyName("contextTierExplicit")] - public bool? ContextTierExplicit { get; set; } + /// Approved effective resource ceilings, or null until approved. + [JsonPropertyName("approved")] + public WorkflowDeclaredLimits? Approved { get; set; } - /// Whether reasoning effort was explicitly selected and should be persisted. - [JsonPropertyName("reasoningEffortExplicit")] - public bool? ReasoningEffortExplicit { get; set; } + /// Whether the durable run state currently passes runtime resume eligibility checks. + [JsonPropertyName("canResume")] + public bool CanResume { get; set; } - /// Filesystem and environment context used to resolve settings persistence. - [JsonPropertyName("settingsContext")] - public ModelPickerSettingsContext SettingsContext { get => field ??= new(); set; } -} + /// Epoch milliseconds when the run completed, or null while nonterminal. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } -/// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSwitchToRequest -{ - /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. - [JsonPropertyName("autoTier")] - public AutoTier? AutoTier { get; set; } + /// Durable resource consumption. + [JsonPropertyName("consumed")] + public WorkflowRunConsumed Consumed { get => field ??= new(); set; } - /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. - [JsonPropertyName("compactionDecision")] - public string? CompactionDecision { get; set; } + /// Epoch milliseconds when the run was created. + [JsonPropertyName("createdAt")] + public long CreatedAt { get; set; } - /// Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. - [JsonPropertyName("contextTier")] - public ContextTier? ContextTier { get; set; } + /// Current phase identity, or null before any phase is entered. + [JsonPropertyName("currentPhase")] + public WorkflowCurrentPhase? CurrentPhase { get; set; } - /// When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). - [JsonPropertyName("deferIfModelChangeQueued")] - public bool? DeferIfModelChangeQueued { get; set; } + /// Resource ceilings declared by the workflow. + [JsonPropertyName("declaredLimits")] + public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } - /// Override individual model capabilities resolved by the runtime. - [JsonPropertyName("modelCapabilities")] - public ModelCapabilitiesOverride? ModelCapabilities { get; set; } + /// Number of phases declared by the workflow. + [JsonPropertyName("declaredPhaseCount")] + public long DeclaredPhaseCount { get; set; } - /// Settings scope used when persisting the selected model. - [JsonPropertyName("modelChangeScope")] - public string? ModelChangeScope { get; set; } + /// Human-readable workflow description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. - [JsonPropertyName("modelId")] - public string ModelId { get; set; } = string.Empty; + /// Number of direct workflow agents currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } - /// Optional settings context and explicit-override flags used to persist a picker selection. - [JsonPropertyName("pickerPersistence")] - public ModelPickerPersistenceRequest? PickerPersistence { get; set; } + /// Epoch milliseconds when this live-overlay snapshot was observed. + [JsonPropertyName("observedAt")] + public long ObservedAt { get; set; } - /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + /// Monotonic durable run revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } - /// Reasoning summary mode to request for supported model clients. - [JsonPropertyName("reasoningSummary")] - public ReasoningSummary? ReasoningSummary { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Optional repository settings scope to persist after the switch commits. - [JsonPropertyName("repoScope")] - public string? RepoScope { get; set; } + /// Epoch milliseconds when execution first started, or null before start. + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } - /// Require the target to be currently available and enabled before applying the switch. - [JsonPropertyName("requireAvailable")] - public bool? RequireAvailable { get; set; } + /// Current workflow run status. + [JsonPropertyName("status")] + public WorkflowRunStatus Status { get; set; } - /// When true, evaluate context-window compaction policy before applying the switch. - [JsonPropertyName("runCompactionPreflight")] - public bool? RunCompactionPreflight { get; set; } + /// Terminal run outcome, or null while nonterminal. + [JsonPropertyName("terminal")] + public WorkflowRunTerminal? Terminal { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Total direct workflow agents spawned across all attempts. + [JsonPropertyName("totalSpawnedAgentCount")] + public long TotalSpawnedAgentCount { get; set; } - /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. - [JsonPropertyName("source")] - public ModelChangeSource? Source { get; set; } + /// Epoch milliseconds when the durable run was last updated. + [JsonPropertyName("updatedAt")] + public long UpdatedAt { get; set; } - /// Output verbosity level to request for supported models. - [JsonPropertyName("verbosity")] - public Verbosity? Verbosity { get; set; } + /// Registered workflow name. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; } -/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// A page of workflow runs in durable creation order. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchAutoTierResult +public sealed class WorkflowListRunsResult { - /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. - [JsonPropertyName("activatingAutoTier")] - public AutoTier? ActivatingAutoTier { get; set; } + /// Whether terminal runs newer than this page exist. + [JsonPropertyName("hasMoreNewer")] + public bool? HasMoreNewer { get; set; } - /// Auto preference currently committed for the session. - [JsonPropertyName("effectiveAutoTier")] - public AutoTier? EffectiveAutoTier { get; set; } + /// Newest terminal-run cursor in this page, or null when the terminal window is empty. + [JsonPropertyName("newestSeq")] + public long? NewestSeq { get; set; } - /// Latest unclaimed Auto preference waiting for a future user turn. - [JsonPropertyName("pendingAutoTier")] - public AutoTier? PendingAutoTier { get; set; } + /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. + [JsonPropertyName("oldestSeq")] + public long? OldestSeq { get; set; } - /// Immediate request status. `pending` means accepted but not committed. - [JsonPropertyName("status")] - public ModelSwitchAutoTierStatus Status { get; set; } + /// Number of terminal runs older than this page. + [JsonPropertyName("omittedOlder")] + public long? OmittedOlder { get; set; } - /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. - [JsonPropertyName("supersededAutoTier")] - public AutoTier? SupersededAutoTier { get; set; } + /// Workflow run summaries in durable creation order. + [JsonPropertyName("runs")] + public IList Runs { get => field ??= []; set; } } -/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. +/// Parameters for paging workflow runs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSwitchAutoTierRequest +internal sealed class WorkflowListRunsRequest { - /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. - [JsonPropertyName("autoTier")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public AutoTier? AutoTier { get; set; } + /// Exclusive forward cursor. + [JsonPropertyName("afterSeq")] + public long? AfterSeq { get; set; } + + /// Exclusive backward cursor. + [JsonPropertyName("beforeSeq")] + public long? BeforeSeq { get; set; } + + /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. + [JsonPropertyName("limit")] + public int? Limit { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. - [JsonPropertyName("source")] - public ModelChangeSource? Source { get; set; } } -/// Managed, repository, and CLI model overrides to overlay onto the session at startup. +/// Prompt-safe durable identity and live status for a direct workflow agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelApplyStartupOverlayRequest +public sealed class WorkflowAgentSummary { - /// Effective default Auto routing preference from user and managed settings. Applies only to fresh sessions and never replaces a per-session selection. - [JsonPropertyName("autoTier")] - public AutoTier? AutoTier { get; set; } + /// Accumulated active agent time in milliseconds. + [JsonPropertyName("activeMs")] + public long ActiveMs { get; set; } - /// Model explicitly selected by the CLI, when provided. - [JsonPropertyName("cliModel")] - public string? CliModel { get; set; } + /// Prompt-safe live activity text. + [JsonPropertyName("activity")] + public string? Activity { get; set; } - /// Whether the overlay is being applied while resuming a deferred session. - [JsonPropertyName("deferredResume")] - public bool? DeferredResume { get; set; } + /// Stable direct-agent identifier. + [JsonPropertyName("agentId")] + public string AgentId { get; set; } = string.Empty; - /// Model required by device-managed policy, when configured. - [JsonPropertyName("deviceManagedModel")] - public string? DeviceManagedModel { get; set; } + /// Registered agent type. + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; - /// Context tier paired with the effective organization-managed model. Applies only when that concrete managed model is selected; it is ignored for Auto and for CLI, resume, or user overrides. - [JsonPropertyName("managedContextTier")] - public string? ManagedContextTier { get; set; } + /// Epoch milliseconds when the agent completed. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } - /// Reasoning effort paired with the effective organization-managed model. Applies only when that concrete managed model is selected; it is ignored for Auto and for CLI, resume, or user overrides. - [JsonPropertyName("managedReasoningEffort")] - public string? ManagedReasoningEffort { get; set; } + /// Friendly, non-unique name intended for display. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. - [JsonPropertyName("policyHelperModel")] - public string? PolicyHelperModel { get; set; } + /// Friendly, non-unique name intended for display. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; - /// Auto routing preference selected by repository settings, when configured. Applied only when the overlay selects the Auto model; beside a concrete model it stays dormant. - [JsonPropertyName("repoAutoTier")] - public string? RepoAutoTier { get; set; } + /// Phase identifier active when the agent was launched, or null. + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } - /// Context tier selected by repository settings, when configured. - [JsonPropertyName("repoContextTier")] - public string? RepoContextTier { get; set; } + /// Model requested when the agent was launched. + [JsonPropertyName("requestedModel")] + public string? RequestedModel { get; set; } - /// Model selected by repository settings, when configured. - [JsonPropertyName("repoModel")] - public string? RepoModel { get; set; } + /// Concrete model resolved for the agent. + [JsonPropertyName("resolvedModel")] + public string? ResolvedModel { get; set; } - /// Reasoning effort selected by repository settings, when configured. - [JsonPropertyName("repoReasoningEffort")] - public string? RepoReasoningEffort { get; set; } + /// Owning workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Model required by server-managed policy, when configured. - [JsonPropertyName("serverManagedModel")] - public string? ServerManagedModel { get; set; } + /// Epoch milliseconds when the agent started. + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Current durable or live agent status. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + /// Tool-call identifier that launched the agent. + [JsonPropertyName("toolCallId")] + public string ToolCallId { get; set; } = string.Empty; } -/// The applied host allowlist and effective session model policy after intersection. +/// Durable lifecycle and timing for one workflow phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSetAllowedModelsResult +public sealed class WorkflowPhaseObservation { - /// Normalized host allowlist. Omitted when the host restriction was cleared, or when a relay client does not return the host policy. - [JsonPropertyName("allowedModels")] - public IList? AllowedModels { get; set; } + /// Completed active time accumulated by this phase in milliseconds. + [JsonPropertyName("accumulatedActiveMs")] + public long AccumulatedActiveMs { get; set; } - /// Effective exact IDs or repository policy patterns after applying the host restriction. Omitted by relay clients that do not return the host policy. - [JsonPropertyName("effectiveAllowedModels")] - public IList? EffectiveAllowedModels { get; set; } + /// Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip timestamp (equal to `startedAt`). + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } - /// Effective deterministic fallback model, when the policy defines one. - [JsonPropertyName("fallbackModel")] - public string? FallbackModel { get; set; } + /// Current live active time for this phase in milliseconds. + [JsonPropertyName("currentActiveMs")] + public long CurrentActiveMs { get; set; } - /// Selected session model after reconciling a now-disallowed concrete selection. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } -} + /// Optional human-readable phase detail. + [JsonPropertyName("detail")] + public string? Detail { get; set; } -/// Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSetAllowedModelsRequest -{ - /// Exact model IDs to permit, or null to clear the host restriction. - [JsonPropertyName("allowedModels")] - public IList? AllowedModels { get; set; } + /// Number of times execution entered this phase. + [JsonPropertyName("entryCount")] + public long EntryCount { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Phase identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; -/// Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSetReasoningEffortResult -{ - /// Reasoning effort level recorded on the session after the update. - [JsonPropertyName("reasoningEffort")] - public string ReasoningEffort { get; set; } = string.Empty; -} + /// Most recent run attempt that entered this phase, or `0` if the phase has never been entered. + [JsonPropertyName("lastEnteredRunAttempt")] + public long LastEnteredRunAttempt { get; set; } -/// Reasoning effort level to apply to the currently selected model. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSetReasoningEffortRequest -{ - /// Reasoning effort level to apply to the currently selected model. The host is responsible for validating the value against the model's supported levels before calling. - [JsonPropertyName("reasoningEffort")] - public string ReasoningEffort { get; set; } = string.Empty; + /// Direct agents in this phase that are currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Zero-based declared phase ordinal, or null for an undeclared phase. + [JsonPropertyName("ordinal")] + public long? Ordinal { get; set; } -/// Cost-category metadata for a CAPI model. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelPriceCategory -{ - /// CAPI model identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip timestamp (equal to `completedAt`). + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } - /// Cost category assigned to the model. - [JsonPropertyName("priceCategory")] - public ModelPickerPriceCategory PriceCategory { get; set; } + /// Derived lifecycle state of the phase. + [JsonPropertyName("status")] + public WorkflowPhaseStatus Status { get; set; } + + /// Human-readable phase title. + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + /// Total direct agents associated with this phase. + [JsonPropertyName("totalAgentCount")] + public long TotalAgentCount { get; set; } } -/// One model provider available to the session — the model analog of the account `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never branches on kind. +/// One durable workflow progress record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelProviderDescriptor +public sealed class WorkflowProgressLine { - /// Opaque, stable provider id, stamped onto every model this provider returns. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Resume attempt that emitted this record. + [JsonPropertyName("attempt")] + public long Attempt { get; set; } - /// The neutral provider kind. + /// Progress record kind. [JsonPropertyName("kind")] - public ModelProviderKind Kind { get; set; } + public WorkflowLogLineKind Kind { get; set; } - /// Human-readable menu label, owned by the runtime so every consumer renders identical text. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; + /// Phase active when the record was emitted, or null before any phase. + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } - /// Stable ordering key for presenting providers in a deterministic sequence. - [JsonPropertyName("ordering")] - public long Ordering { get; set; } + /// Epoch milliseconds when the record was persisted. + [JsonPropertyName("recordedAt")] + public long RecordedAt { get; set; } + + /// Global monotonic sequence number within the run. + [JsonPropertyName("seq")] + public long Seq { get; set; } + + /// Prompt-safe progress text. + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; } -/// The list of models available to this session. +/// A bidirectional page of workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelList +public sealed class WorkflowProgressPage { - /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). - [JsonPropertyName("list")] - public IList List { get => field ??= []; set; } - - /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. - [JsonPropertyName("modelPriceCategories")] - public IList? ModelPriceCategories { get; set; } + /// Whether progress records newer than this page exist. + [JsonPropertyName("hasMoreNewer")] + public bool HasMoreNewer { get; set; } - /// The model providers available to this session, in ordering order, resolved from the account roster; empty when no provider is entitled (logged out / seatless). Each model in `list` carries its own provider reference; this roster gives the deterministic provider sequence and lets a consumer group by provider without deriving ordering from the model list. Central code never branches on a provider kind. - [JsonPropertyName("providers")] - public IList? Providers { get; set; } + /// Whether progress records older than this page exist. + [JsonPropertyName("hasMoreOlder")] + public bool HasMoreOlder { get; set; } - /// Per-quota snapshots returned alongside the model list, keyed by quota type. - [JsonPropertyName("quotaSnapshots")] - public IDictionary? QuotaSnapshots { get; set; } -} + /// Newest sequence number in this page, or null when empty. + [JsonPropertyName("newestSeq")] + public long? NewestSeq { get; set; } -/// RPC data type for SessionModelList operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelListRequest -{ - /// If true, bypasses the per-session model list cache and re-fetches from CAPI. - [JsonPropertyName("skipCache")] - public bool? SkipCache { get; set; } -} + /// Oldest sequence number in this page, or null when empty. + [JsonPropertyName("oldestSeq")] + public long? OldestSeq { get; set; } -/// RPC data type for SessionModelListRequestWithSession operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModelListRequestWithSession -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Progress records in sequence order. + [JsonPropertyName("records")] + public IList Records { get => field ??= []; set; } - /// If true, bypasses the per-session model list cache and re-fetches from CAPI. - [JsonPropertyName("skipCache")] - public bool? SkipCache { get; set; } + /// Run revision reflected by this page. + [JsonPropertyName("revision")] + public long Revision { get; set; } } -/// Identifies the target session. +/// Full workflow run observability detail. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModeGetRequest +public sealed class WorkflowRunDetail { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Epoch milliseconds when the current active segment started, or null while inactive. + [JsonPropertyName("activeSegmentStartedAt")] + public long? ActiveSegmentStartedAt { get; set; } -/// Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModeSetResult -{ - /// Whether the host should arm an interactive continuation after the mode change. - [JsonPropertyName("armInteractiveContinuation")] - public bool? ArmInteractiveContinuation { get; set; } + /// Durable identities and live statuses for direct workflow agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } - /// Compaction confirmation required before the mode change can complete. - [JsonPropertyName("confirmation")] - public ModelSwitchConfirmation? Confirmation { get; set; } + /// Approved effective resource ceilings, or null until approved. + [JsonPropertyName("approved")] + public WorkflowDeclaredLimits? Approved { get; set; } - /// Whether the host must defer implementing the requested mode change. - [JsonPropertyName("deferImplementation")] - public bool? DeferImplementation { get; set; } + /// Whether the durable run state currently passes runtime resume eligibility checks. + [JsonPropertyName("canResume")] + public bool CanResume { get; set; } - /// Deprecation warnings associated with the model selected by the mode change. - [JsonPropertyName("deprecationWarnings")] - public IList? DeprecationWarnings { get; set; } + /// Epoch milliseconds when the run completed, or null while nonterminal. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } - /// User-facing outcome message for the model switch triggered by the mode change. - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Durable resource consumption. + [JsonPropertyName("consumed")] + public WorkflowRunConsumed Consumed { get => field ??= new(); set; } - /// Whether the requested mode was applied to the session. False only when an 'expectedMode' precondition did not hold, in which case any model change reported alongside it was still applied. - [JsonPropertyName("modeApplied")] - public bool? ModeApplied { get; set; } + /// Epoch milliseconds when the run was created. + [JsonPropertyName("createdAt")] + public long CreatedAt { get; set; } - /// Whether applying the mode changed the active model. - [JsonPropertyName("modelChanged")] - public bool ModelChanged { get; set; } + /// Current phase identity, or null before any phase is entered. + [JsonPropertyName("currentPhase")] + public WorkflowCurrentPhase? CurrentPhase { get; set; } - /// Lifecycle status of the requested mode change. - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; + /// Resource ceilings declared by the workflow. + [JsonPropertyName("declaredLimits")] + public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } - /// User-facing warning produced while applying the mode change. - [JsonPropertyName("warning")] - public string? Warning { get; set; } -} + /// Number of phases declared by the workflow. + [JsonPropertyName("declaredPhaseCount")] + public long DeclaredPhaseCount { get; set; } -/// Agent interaction mode to apply to the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModeSetRequest -{ - /// Explicit response to a model-switch compaction preflight. - [JsonPropertyName("compactionDecision")] - public string? CompactionDecision { get; set; } + /// Human-readable workflow description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Mode the session must currently be in for the change to apply. When set and the session is in a different mode the request is a no-op and reports status 'unchanged'. - [JsonPropertyName("expectedMode")] - public SessionMode? ExpectedMode { get; set; } + /// Number of direct workflow agents currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } - /// Session whose plan-mode base state should be inherited. - [JsonPropertyName("inheritPlanBaseFromSessionId")] - public string? InheritPlanBaseFromSessionId { get; set; } + /// Epoch milliseconds when this live-overlay snapshot was observed. + [JsonPropertyName("observedAt")] + public long ObservedAt { get; set; } - /// The session mode the agent is operating in. - [JsonPropertyName("mode")] - public SessionMode Mode { get; set; } + /// Lifecycle and timing observations for each workflow phase. + [JsonPropertyName("phases")] + public IList Phases { get => field ??= []; set; } - /// Whether the selected plan model should be persisted. - [JsonPropertyName("persistPlanSelection")] - public bool? PersistPlanSelection { get; set; } + /// Bidirectional page of durable workflow progress. + [JsonPropertyName("progress")] + public WorkflowProgressPage Progress { get => field ??= new(); set; } - /// Settings context used when persisting the selected plan model. - [JsonPropertyName("pickerSettingsContext")] - public ModelPickerSettingsContext? PickerSettingsContext { get; set; } + /// Monotonic durable run revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } - /// Context tier to use with the dedicated plan model. - [JsonPropertyName("planContextTier")] - public string? PlanContextTier { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Action to perform when leaving plan mode. - [JsonPropertyName("planExitAction")] - public string? PlanExitAction { get; set; } + /// Epoch milliseconds when execution first started, or null before start. + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } - /// Dedicated model to use in plan mode, when configured. - [JsonPropertyName("planModel")] - public string? PlanModel { get; set; } + /// Current workflow run status. + [JsonPropertyName("status")] + public WorkflowRunStatus Status { get; set; } - /// Whether a dedicated plan model is configured. - [JsonPropertyName("planModelConfigured")] - public bool? PlanModelConfigured { get; set; } + /// Terminal run outcome, or null while nonterminal. + [JsonPropertyName("terminal")] + public WorkflowRunTerminal? Terminal { get; set; } - /// Reasoning effort to use with the dedicated plan model. - [JsonPropertyName("planReasoningEffort")] - public string? PlanReasoningEffort { get; set; } + /// Total direct workflow agents spawned across all attempts. + [JsonPropertyName("totalSpawnedAgentCount")] + public long TotalSpawnedAgentCount { get; set; } - /// Whether leaving plan mode should restore the session's previous model. - [JsonPropertyName("restorePlanModel")] - public bool? RestorePlanModel { get; set; } + /// Epoch milliseconds when the durable run was last updated. + [JsonPropertyName("updatedAt")] + public long UpdatedAt { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Registered workflow name. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; } -/// The session's friendly name, or null when not yet set. +/// Parameters for paging workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NameGetResult +internal sealed class WorkflowGetRunProgressRequest { - /// The session name (user-set or auto-generated), or null if not yet set. - [JsonPropertyName("name")] - public string? Name { get; set; } -} + /// Exclusive forward cursor. + [JsonPropertyName("afterSeq")] + public long? AfterSeq { get; set; } + + /// Exclusive backward cursor. + [JsonPropertyName("beforeSeq")] + public long? BeforeSeq { get; set; } + + /// Maximum records to return. Defaults to 200 and is capped at 500. + [JsonPropertyName("limit")] + public int? Limit { get; set; } + + /// Optional phase identifier used to scope records and cursors. + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionNameGetRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// New friendly name to apply to the session. +/// Parameters for cancelling a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class NameSetRequest +internal sealed class WorkflowCancelRequest { - /// New session name (1–100 characters, trimmed of leading/trailing whitespace). - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [MaxLength(100)] - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the auto-generated summary was applied as the session's name. +/// Parameters for pausing a running workflow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NameSetAutoResult +internal sealed class WorkflowPauseRequest { - /// Whether the auto-generated summary was persisted. False if the session already has a user-set name, the summary normalized to empty, or the session does not have a workspace. - [JsonPropertyName("applied")] - public bool Applied { get; set; } -} + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; -/// Auto-generated session summary to apply as the session's name when no user-set name exists. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class NameSetAutoRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Auto-generated session summary. Empty/whitespace-only values are ignored; values are trimmed before persisting. - [JsonPropertyName("summary")] - public string Summary { get; set; } = string.Empty; } -/// Existence, contents, and resolved path of the session plan file. +/// RPC data type for SessionWorkflowPauseAtCheckpoint operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadResult +internal sealed class SessionWorkflowPauseAtCheckpointResult { - /// The content of the plan file, or null if it does not exist. - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// Whether the plan file exists in the workspace. - [JsonPropertyName("exists")] - public bool Exists { get; set; } - - /// Absolute file path of the plan file, or null if workspace is not enabled. - [JsonPropertyName("path")] - public string? Path { get; set; } + /// Whether this execution attempt must pause or may continue. + [JsonPropertyName("action")] + public WorkflowPauseCheckpointAction Action { get; set; } } -/// Identifies the target session. +/// Parameters for an owned durable pause checkpoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanReadRequest +internal sealed class WorkflowPauseCheckpointRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Opaque token identifying the execution attempt that reached the checkpoint. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; -/// Replacement contents to write to the session plan file. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PlanUpdateRequest -{ - /// The new content for the plan file. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Stable author-defined checkpoint key. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Acknowledgement that a workflow request was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanDeleteRequest +public sealed class WorkflowAckResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; } -/// A single todo row read from the session SQL `todos` table. All fields are optional because the SQL schema is best-effort and the agent may not have populated every column. +/// One ordered workflow progress line. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanSqlTodosRow +public sealed class WorkflowLogLine { - /// Todo creation time, as stored by the session SQL schema's `datetime('now')` default: `YYYY-MM-DD HH:MM:SS` in UTC. Lets clients attribute todos to the work item that created them (e.g. scoping a goal's progress to the todos it produced) rather than to the whole session. - [JsonPropertyName("createdAt")] - public string? CreatedAt { get; set; } - - /// Todo description. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// Todo identifier. - [JsonPropertyName("id")] - public string? Id { get; set; } + /// Progress line kind. + [JsonPropertyName("kind")] + public WorkflowLogLineKind Kind { get; set; } - /// Todo status. - [JsonPropertyName("status")] - public string? Status { get; set; } + /// Monotonic sequence number within the workflow run. + [JsonPropertyName("seq")] + public long Seq { get; set; } - /// Todo title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Progress text. + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; } -/// Todo rows read from the session SQL database. Empty when no session database is available. +/// Parameters for recording workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadSqlTodosResult +internal sealed class WorkflowLogRequest { - /// Rows from the session SQL todos table, ordered by creation time with insertion order used to break ties when available and id used for WITHOUT ROWID tables. - [JsonPropertyName("rows")] - public IList Rows { get => field ??= []; set; } -} + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; + + /// Ordered progress lines to append. + [JsonPropertyName("lines")] + public IList Lines { get => field ??= []; set; } + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanReadSqlTodosRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single dependency edge read from the session SQL `todo_deps` table, indicating that one todo must complete before another. +/// Result of one workflow-scoped subagent call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanSqlTodoDependency +public sealed class WorkflowAgentResult { - /// ID of the todo it depends on. - [JsonPropertyName("dependsOn")] - public string DependsOn { get; set; } = string.Empty; - - /// ID of the todo that has the dependency. - [JsonPropertyName("todoId")] - public string TodoId { get; set; } = string.Empty; + /// Agent result, omitted when the agent produced no result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } } -/// Todo rows + dependency edges read from the session SQL database. +/// Options for one workflow-scoped subagent call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadSqlTodosWithDependenciesResult +public sealed class WorkflowAgentOptions { - /// Edges from the session SQL todo_deps table. Empty when no database, no todo_deps table, or the SELECT failed. Read independently from `rows`, so a broken todo_deps table does not affect the rows result and vice versa. - [JsonPropertyName("dependencies")] - public IList Dependencies { get => field ??= []; set; } + /// Optional built-in or custom agent name whose definition configures the subagent. + [JsonPropertyName("agent")] + public string? Agent { get; set; } - /// Rows from the session SQL todos table, ordered by creation time with insertion order used to break ties when available and id used for WITHOUT ROWID tables. Empty when no database, no todos table, or the SELECT failed. - [JsonPropertyName("rows")] - public IList Rows { get => field ??= []; set; } -} + /// Optional context tier override for the subagent. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanReadSqlTodosWithDependenciesRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} - -/// RPC data type for WorkspacesGetWorkspaceResultWorkspace operations. -public sealed class WorkspacesGetWorkspaceResultWorkspace -{ - /// Current Git branch. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// Whether the per-session Chronicle upgrade prompt was dismissed for the workspace. - [JsonPropertyName("chronicle_sync_dismissed")] - public bool? ChronicleSyncDismissed { get; set; } - - /// Name of the client that created the workspace. - [JsonPropertyName("client_name")] - public string? ClientName { get; set; } - - /// Timestamp when the workspace was created. - [JsonPropertyName("created_at")] - public DateTimeOffset? CreatedAt { get; set; } - - /// Current working directory associated with the workspace. - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } - - /// Git repository root associated with the workspace. - [JsonPropertyName("git_root")] - public string? GitRoot { get; set; } - - /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. - [JsonPropertyName("host_type")] - public WorkspacesWorkspaceDetailsHostType? HostType { get; set; } - - /// Stable workspace identifier. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Most recent Mission Control event identifier observed for the workspace. - [JsonPropertyName("mc_last_event_id")] - public string? McLastEventId { get; set; } + /// Optional label distinguishing otherwise identical memoized agent calls. + [JsonPropertyName("label")] + public string? Label { get; set; } - /// Mission Control session identifier associated with the workspace. - [JsonPropertyName("mc_session_id")] - public string? McSessionId { get; set; } + /// Optional model identifier for the subagent. + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Mission Control task identifier associated with the workspace. - [JsonPropertyName("mc_task_id")] - public string? McTaskId { get; set; } + /// Optional reasoning effort override for the subagent. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } - /// Workspace display name. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Optional JSON Schema for structured agent output. + [JsonPropertyName("schema")] + public JsonElement? Schema { get; set; } +} - /// Whether the workspace session can be steered remotely. - [JsonPropertyName("remote_steerable")] - public bool? RemoteSteerable { get; set; } +/// Parameters for one workflow-scoped subagent call. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowAgentRequest +{ + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; - /// Repository identifier associated with the workspace. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// Subagent execution options. + [JsonPropertyName("opts")] + public WorkflowAgentOptions Opts { get => field ??= new(); set; } - /// Number of persisted summaries in the workspace. - [JsonPropertyName("summary_count")] - public long? SummaryCount { get; set; } + /// Prompt to send to the subagent. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Timestamp when the workspace was last updated. - [JsonPropertyName("updated_at")] - public DateTimeOffset? UpdatedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Whether the workspace name was explicitly chosen by the user. - [JsonPropertyName("user_named")] - public bool? UserNamed { get; set; } + /// Workflow run identifier that owns the subagent. + [JsonPropertyName("workflowRunId")] + public string WorkflowRunId { get; set; } = string.Empty; } -/// Current workspace metadata for the session, including its absolute filesystem path when available. +/// Result of reading a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesGetWorkspaceResult +public sealed class WorkflowJournalGetResult { - /// Absolute filesystem path to the workspace directory. Omitted when the session has no workspace (e.g. remote sessions). - [JsonPropertyName("path")] - public string? Path { get; set; } + /// Whether the journal contained the requested key. + [JsonPropertyName("hit")] + public bool Hit { get; set; } - /// Current workspace metadata, or null if not available. - [JsonPropertyName("workspace")] - public WorkspacesGetWorkspaceResultWorkspace? Workspace { get; set; } + /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. + [JsonPropertyName("resultJson")] + public JsonElement? ResultJson { get; set; } } -/// Identifies the target session. +/// Parameters for reading a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesGetWorkspaceRequest +internal sealed class WorkflowJournalGetRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; -/// Workspace metadata fields to update. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesUpdateMetadataRequest -{ - /// Opaque workspace context supplied by the session host. - [JsonPropertyName("context")] - public JsonElement? Context { get; set; } + /// Namespaced journal key. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; - /// Optional workspace display name override. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Optional session context used when creating a local workspace. +/// Parameters for storing a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesEnsureRequest +internal sealed class WorkflowJournalPutRequest { - /// Opaque workspace context supplied by the session host. - [JsonPropertyName("context")] - public JsonElement? Context { get; set; } + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; + + /// Namespaced journal key. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; + + /// JSON result to memoize. + [JsonPropertyName("resultJson")] + public JsonElement ResultJson { get; set; } + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Relative paths of files stored in the session workspace files directory. +/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesListFilesResult +public sealed class CurrentModel { - /// Slash-separated relative file paths in the workspace files directory. - [JsonPropertyName("files")] - public IList Files { get => field ??= []; set; } + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } + + /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + + /// Context tier for models that support multiple context-window sizes. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } + + /// Currently active model identifier. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } + + /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } + + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + [JsonPropertyName("planBaseModelId")] + public string? PlanBaseModelId { get; set; } + + /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesListFilesRequest +internal sealed class SessionModelGetCurrentRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Contents of the requested workspace file as a UTF-8 string. +/// RPC data type for ModelSwitchConfirmation operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadFileResult +public sealed class ModelSwitchConfirmation { - /// File content as a UTF-8 string. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; -} + /// Current conversation token count before switching models. + [JsonPropertyName("currentTokens")] + public double CurrentTokens { get; set; } -/// Relative path of the workspace file to read. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesReadFileRequest -{ - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Target model token limit used by the compaction preflight. + [JsonPropertyName("targetLimit")] + public double TargetLimit { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Display name of the model that requires compaction confirmation. + [JsonPropertyName("targetModelDisplayName")] + public string TargetModelDisplayName { get; set; } = string.Empty; } -/// Relative path and UTF-8 content for the workspace file to create or overwrite. +/// The model identifier active on the session after the switch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesCreateFileRequest +public sealed class ModelSwitchToResult { - /// File content to write as a UTF-8 string. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Compaction confirmation projection when status is confirmation_required. + [JsonPropertyName("confirmation")] + public ModelSwitchConfirmation? Confirmation { get; set; } - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// True when the switch was deferred (enqueued as a cancellable `/model` command) because a turn was active or another model change was already queued, rather than applied immediately. When true, the session's live model is unchanged until the queued change drains. + [JsonPropertyName("deferred")] + public bool? Deferred { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Deprecation warnings associated with the selected model or options. + [JsonPropertyName("deprecationWarnings")] + public IList? DeprecationWarnings { get; set; } -/// Filesystem metadata for a path in the session workspace files directory. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesStatFileResult -{ - /// Creation time in Unix epoch milliseconds. - [JsonPropertyName("birthtimeMs")] - public double BirthtimeMs { get; set; } + /// User-facing outcome message for the model switch. + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Whether the path identifies a directory. - [JsonPropertyName("isDirectory")] - public bool IsDirectory { get; set; } + /// Currently active model identifier after the switch. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } - /// Whether the path identifies a regular file. - [JsonPropertyName("isFile")] - public bool IsFile { get; set; } + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + [JsonPropertyName("modelState")] + public CurrentModel? ModelState { get; set; } - /// Last modification time in Unix epoch milliseconds. - [JsonPropertyName("mtimeMs")] - public double MtimeMs { get; set; } + /// Persistence failure encountered after applying the model switch. + [JsonPropertyName("persistenceError")] + public string? PersistenceError { get; set; } - /// Size in bytes. - [JsonPropertyName("size")] - public double Size { get; set; } + /// Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. + [JsonPropertyName("queueId")] + public string? QueueId { get; set; } + + /// Lifecycle result for the requested switch. + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// User-facing warning produced while applying the model switch. + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Relative path of the workspace file or directory to inspect. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesStatFileRequest +/// Environment variables consulted while resolving model-picker settings. +public sealed class ModelPickerSettingsContextEnvironment { - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; } -/// Directory to create within the session workspace files directory. +/// Filesystem and environment context used to resolve model-picker settings. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesCreateDirectoryRequest +public sealed class ModelPickerSettingsContext { - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Optional Copilot configuration directory containing persisted settings. + [JsonPropertyName("configDir")] + public string? ConfigDir { get; set; } - /// Whether to create missing parent directories. Defaults to false. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// Environment variables consulted while resolving model-picker settings. + [JsonPropertyName("environment")] + public ModelPickerSettingsContextEnvironment Environment { get => field ??= new(); set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// User home directory used when resolving persisted settings. + [JsonPropertyName("homeDirectory")] + public string HomeDirectory { get; set; } = string.Empty; } -/// File or directory to remove from the session workspace files directory. +/// RPC data type for ModelPickerPersistence operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesRemovePathRequest +public sealed class ModelPickerPersistenceRequest { - /// Whether a missing path should be treated as success. Defaults to false. - [JsonPropertyName("force")] - public bool? Force { get; set; } - - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Whether context tier was explicitly selected and should be persisted. + [JsonPropertyName("contextTierExplicit")] + public bool? ContextTierExplicit { get; set; } - /// Whether to remove directory contents recursively. Defaults to false. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// Whether reasoning effort was explicitly selected and should be persisted. + [JsonPropertyName("reasoningEffortExplicit")] + public bool? ReasoningEffortExplicit { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Filesystem and environment context used to resolve settings persistence. + [JsonPropertyName("settingsContext")] + public ModelPickerSettingsContext SettingsContext { get => field ??= new(); set; } } -/// Source and destination paths for a rename within the session workspace files directory. +/// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesRenamePathRequest +internal sealed class ModelSwitchToRequest { - /// Slash-separated destination path relative to the workspace files directory. - [JsonPropertyName("destination")] - public string Destination { get; set; } = string.Empty; + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. + [JsonPropertyName("compactionDecision")] + public string? CompactionDecision { get; set; } - /// Slash-separated source path relative to the workspace files directory. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; -} + /// Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } -/// Workspace checkpoint metadata with assigned number, human-readable title, and checkpoint filename. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesCheckpoints -{ - /// Filename of the checkpoint within the workspace checkpoints directory. - [JsonPropertyName("filename")] - public string Filename { get; set; } = string.Empty; + /// When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). + [JsonPropertyName("deferIfModelChangeQueued")] + public bool? DeferIfModelChangeQueued { get; set; } - /// Checkpoint number assigned by the workspace manager. - [JsonPropertyName("number")] - public long Number { get; set; } + /// Override individual model capabilities resolved by the runtime. + [JsonPropertyName("modelCapabilities")] + public ModelCapabilitiesOverride? ModelCapabilities { get; set; } - /// Human-readable checkpoint title. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; -} + /// Settings scope used when persisting the selected model. + [JsonPropertyName("modelChangeScope")] + public string? ModelChangeScope { get; set; } -/// Workspace checkpoints in chronological order; empty when the workspace is not enabled. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesListCheckpointsResult -{ - /// Workspace checkpoints in chronological order. Empty when workspace is not enabled. - [JsonPropertyName("checkpoints")] - public IList Checkpoints { get => field ??= []; set; } -} + /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesListCheckpointsRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Optional settings context and explicit-override flags used to persist a picker selection. + [JsonPropertyName("pickerPersistence")] + public ModelPickerPersistenceRequest? PickerPersistence { get; set; } -/// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadCheckpointResult -{ - /// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. - [JsonPropertyName("content")] - public string? Content { get; set; } -} + /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. Pass null to clear any session effort override and fall back to the model's default. When omitted, the session's current effort is kept. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } -/// Checkpoint number to read. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesReadCheckpointRequest -{ - /// Checkpoint number to read. - [JsonPropertyName("number")] - public long Number { get; set; } + /// Reasoning summary mode to request for supported model clients. + [JsonPropertyName("reasoningSummary")] + public ReasoningSummary? ReasoningSummary { get; set; } + + /// Optional repository settings scope to persist after the switch commits. + [JsonPropertyName("repoScope")] + public string? RepoScope { get; set; } + + /// Require the target to be currently available and enabled before applying the switch. + [JsonPropertyName("requireAvailable")] + public bool? RequireAvailable { get; set; } + + /// When true, evaluate context-window compaction policy before applying the switch. + [JsonPropertyName("runCompactionPreflight")] + public bool? RunCompactionPreflight { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; -} -/// Metadata for the persisted summary. -public sealed class WorkspacesAddSummaryResultSummary -{ -} + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. + [JsonPropertyName("source")] + public ModelChangeSource? Source { get; set; } -/// Refreshed metadata for the containing workspace. -public sealed class WorkspacesAddSummaryResultWorkspace -{ + /// Output verbosity level to request for supported models. + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } } -/// Persisted summary metadata and refreshed workspace metadata. +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesAddSummaryResult +public sealed class ModelSwitchAutoTierResult { - /// Metadata for the persisted summary. - [JsonPropertyName("summary")] - public WorkspacesAddSummaryResultSummary? Summary { get; set; } + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } - /// Refreshed metadata for the containing workspace. - [JsonPropertyName("workspace")] - public WorkspacesAddSummaryResultWorkspace? Workspace { get; set; } + /// Auto preference currently committed for the session. + [JsonPropertyName("effectiveAutoTier")] + public AutoTier? EffectiveAutoTier { get; set; } + + /// Latest unclaimed Auto preference waiting for a future user turn. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } + + /// Immediate request status. `pending` means accepted but not committed. + [JsonPropertyName("status")] + public ModelSwitchAutoTierStatus Status { get; set; } + + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + [JsonPropertyName("supersededAutoTier")] + public AutoTier? SupersededAutoTier { get; set; } } -/// Compaction summary checkpoint to persist. +/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesAddSummaryRequest +internal sealed class ModelSwitchAutoTierRequest { - /// Markdown summary content to persist. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + [JsonPropertyName("autoTier")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public AutoTier? AutoTier { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Summary title shown in checkpoint listings. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + [JsonPropertyName("source")] + public ModelChangeSource? Source { get; set; } } -/// Rollback point for local workspace summaries. +/// Managed, repository, and CLI model overrides to overlay onto the session at startup. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesTruncateSummariesRequest +internal sealed class ModelApplyStartupOverlayRequest { - /// Number of newest summaries to keep. - [JsonPropertyName("keepCount")] - public long KeepCount { get; set; } + /// Effective default Auto routing preference from user and managed settings. Applies only to fresh sessions and never replaces a per-session selection. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + + /// Model explicitly selected by the CLI, when provided. + [JsonPropertyName("cliModel")] + public string? CliModel { get; set; } + + /// Whether the overlay is being applied while resuming a deferred session. + [JsonPropertyName("deferredResume")] + public bool? DeferredResume { get; set; } + + /// Model required by device-managed policy, when configured. + [JsonPropertyName("deviceManagedModel")] + public string? DeviceManagedModel { get; set; } + + /// Context tier paired with the effective organization-managed model. Applies only when that concrete managed model is selected; it is ignored for Auto and for CLI, resume, or user overrides. + [JsonPropertyName("managedContextTier")] + public string? ManagedContextTier { get; set; } + + /// Reasoning effort paired with the effective organization-managed model. Applies only when that concrete managed model is selected; it is ignored for Auto and for CLI, resume, or user overrides. + [JsonPropertyName("managedReasoningEffort")] + public string? ManagedReasoningEffort { get; set; } + + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + [JsonPropertyName("policyHelperModel")] + public string? PolicyHelperModel { get; set; } + + /// Auto routing preference selected by repository settings, when configured. Applied only when the overlay selects the Auto model; beside a concrete model it stays dormant. + [JsonPropertyName("repoAutoTier")] + public string? RepoAutoTier { get; set; } + + /// Context tier selected by repository settings, when configured. + [JsonPropertyName("repoContextTier")] + public string? RepoContextTier { get; set; } + + /// Model selected by repository settings, when configured. + [JsonPropertyName("repoModel")] + public string? RepoModel { get; set; } + + /// Reasoning effort selected by repository settings, when configured. + [JsonPropertyName("repoReasoningEffort")] + public string? RepoReasoningEffort { get; set; } + + /// Model required by server-managed policy, when configured. + [JsonPropertyName("serverManagedModel")] + public string? ServerManagedModel { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Autopilot objective file content, or null when missing. +/// The applied host allowlist and effective session model policy after intersection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadAutopilotObjectiveResult +public sealed class ModelSetAllowedModelsResult { - /// Autopilot objective file content, or null when missing. - [JsonPropertyName("content")] - public string? Content { get; set; } + /// Normalized host allowlist. Omitted when the host restriction was cleared, or when a relay client does not return the host policy. + [JsonPropertyName("allowedModels")] + public IList? AllowedModels { get; set; } + + /// Effective exact IDs or repository policy patterns after applying the host restriction. Omitted by relay clients that do not return the host policy. + [JsonPropertyName("effectiveAllowedModels")] + public IList? EffectiveAllowedModels { get; set; } + + /// Effective deterministic fallback model, when the policy defines one. + [JsonPropertyName("fallbackModel")] + public string? FallbackModel { get; set; } + + /// Selected session model after reconciling a now-disallowed concrete selection. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } } -/// Identifies the target session. +/// Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesReadAutopilotObjectiveRequest +internal sealed class ModelSetAllowedModelsRequest { + /// Exact model IDs to permit, or null to clear the host restriction. + [JsonPropertyName("allowedModels")] + public IList? AllowedModels { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of writing the autopilot objective file. +/// Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesWriteAutopilotObjectiveResult +public sealed class ModelSetReasoningEffortResult { - /// Filesystem operation performed. - [JsonPropertyName("operation")] - public string Operation { get; set; } = string.Empty; + /// Reasoning effort level recorded on the session after the update. + [JsonPropertyName("reasoningEffort")] + public string ReasoningEffort { get; set; } = string.Empty; } -/// Autopilot objective file content to persist. +/// Reasoning effort level to apply to the currently selected model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesWriteAutopilotObjectiveRequest +internal sealed class ModelSetReasoningEffortRequest { - /// Autopilot objective file content. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Reasoning effort level to apply to the currently selected model. The host is responsible for validating the value against the model's supported levels before calling. + [JsonPropertyName("reasoningEffort")] + public string ReasoningEffort { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of deleting the autopilot objective file. +/// Cost-category metadata for a CAPI model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesDeleteAutopilotObjectiveResult +public sealed class SessionModelPriceCategory { - /// True when a file was deleted. - [JsonPropertyName("deleted")] - public bool Deleted { get; set; } -} + /// CAPI model identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesDeleteAutopilotObjectiveRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Cost category assigned to the model. + [JsonPropertyName("priceCategory")] + public ModelPickerPriceCategory PriceCategory { get; set; } } -/// Whether the autopilot objective file exists. +/// One model provider available to the session — the model analog of the account `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never branches on kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesAutopilotObjectiveExistsResult +public sealed class ModelProviderDescriptor { - /// True when the objective file exists. - [JsonPropertyName("exists")] - public bool Exists { get; set; } + /// Opaque, stable provider id, stamped onto every model this provider returns. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// The neutral provider kind. + [JsonPropertyName("kind")] + public ModelProviderKind Kind { get; set; } + + /// Human-readable menu label, owned by the runtime so every consumer renders identical text. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; + + /// Stable ordering key for presenting providers in a deterministic sequence. + [JsonPropertyName("ordering")] + public long Ordering { get; set; } } -/// Identifies the target session. +/// The list of models available to this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesAutopilotObjectiveExistsRequest +public sealed class SessionModelList { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). + [JsonPropertyName("list")] + public IList List { get => field ??= []; set; } -/// RPC data type for WorkspacesSaveLargePasteResultSaved operations. -public sealed class WorkspacesSaveLargePasteResultSaved -{ - /// Filename within the workspace files directory. - [JsonPropertyName("filename")] - public string Filename { get; set; } = string.Empty; + /// Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable. + [JsonPropertyName("modelPriceCategories")] + public IList? ModelPriceCategories { get; set; } - /// Absolute filesystem path to the saved paste file. - [JsonPropertyName("filePath")] - public string FilePath { get; set; } = string.Empty; + /// The model providers available to this session, in ordering order, resolved from the account roster; empty when no provider is entitled (logged out / seatless). Each model in `list` carries its own provider reference; this roster gives the deterministic provider sequence and lets a consumer group by provider without deriving ordering from the model list. Central code never branches on a provider kind. + [JsonPropertyName("providers")] + public IList? Providers { get; set; } - /// Size of the saved file in bytes. - [JsonPropertyName("sizeBytes")] - public long SizeBytes { get; set; } + /// Per-quota snapshots returned alongside the model list, keyed by quota type. + [JsonPropertyName("quotaSnapshots")] + public IDictionary? QuotaSnapshots { get; set; } } -/// Descriptor for the saved paste file, or null when the workspace is unavailable. +/// RPC data type for SessionModelList operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesSaveLargePasteResult +public sealed class SessionModelListRequest { - /// Saved-paste descriptor, or null when the workspace is unavailable (e.g. CCA runtime, non-infinite sessions, remote sessions). - [JsonPropertyName("saved")] - public WorkspacesSaveLargePasteResultSaved? Saved { get; set; } + /// If true, bypasses the per-session model list cache and re-fetches from CAPI. + [JsonPropertyName("skipCache")] + public bool? SkipCache { get; set; } } -/// Pasted content to save as a UTF-8 file in the session workspace. +/// RPC data type for SessionModelListRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesSaveLargePasteRequest +internal sealed class SessionModelListRequestWithSession { - /// Pasted content to save as a UTF-8 file. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// If true, bypasses the per-session model list cache and re-fetches from CAPI. + [JsonPropertyName("skipCache")] + public bool? SkipCache { get; set; } } -/// A single changed file and its unified diff. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspaceDiffFileChange +internal sealed class SessionModeGetRequest { - /// Type of change represented by this file diff. - [JsonPropertyName("changeType")] - public WorkspaceDiffFileChangeType ChangeType { get; set; } - - /// Unified diff content for the file. Empty when the diff was truncated. - [JsonPropertyName("diff")] - public string Diff { get; set; } = string.Empty; - - /// Whether the diff content was omitted because it exceeded the per-file size limit. - [JsonPropertyName("isTruncated")] - public bool? IsTruncated { get; set; } - - /// Original file path for renamed files. - [JsonPropertyName("oldPath")] - public string? OldPath { get; set; } - - /// Path to the changed file, relative to the workspace root when the file lives under it. A file changed outside the workspace root keeps a `../`-relative path, or an absolute path when no relative path exists (for example a different Windows drive). - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Workspace diff result for the requested mode. +/// Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspaceDiffResult +public sealed class ModeSetResult { - /// Default branch used for a branch diff, when branch mode was requested. - [JsonPropertyName("baseBranch")] - public string? BaseBranch { get; set; } + /// Whether the host should arm an interactive continuation after the mode change. + [JsonPropertyName("armInteractiveContinuation")] + public bool? ArmInteractiveContinuation { get; set; } - /// Changed files and their unified diffs. - [JsonPropertyName("changes")] - public IList Changes { get => field ??= []; set; } + /// Compaction confirmation required before the mode change can complete. + [JsonPropertyName("confirmation")] + public ModelSwitchConfirmation? Confirmation { get; set; } - /// Whether the requested diff fell back to unstaged changes, either because branch diff failed or session diff was unavailable. - [JsonPropertyName("isFallback")] - public bool IsFallback { get; set; } + /// Whether the host must defer implementing the requested mode change. + [JsonPropertyName("deferImplementation")] + public bool? DeferImplementation { get; set; } - /// Effective mode used for the returned changes. - [JsonPropertyName("mode")] - public WorkspaceDiffMode Mode { get; set; } + /// Deprecation warnings associated with the model selected by the mode change. + [JsonPropertyName("deprecationWarnings")] + public IList? DeprecationWarnings { get; set; } - /// Diff mode requested by the client. - [JsonPropertyName("requestedMode")] - public WorkspaceDiffMode RequestedMode { get; set; } + /// User-facing outcome message for the model switch triggered by the mode change. + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Why the session diff could not be produced, when applicable. Set only when `session` mode was requested and `isFallback` is true, so a client can tell the permanent `file-change-tracking-disabled` apart from the transient `session-busy`, which the same request answers once the session settles. Never set for `unstaged` or `branch` mode, and never `unsupported-remote-session`: a remote session's captures live on its own host, so a `session`-mode diff is rejected for one rather than answered with a controller-side fallback. - [JsonPropertyName("unavailableReason")] - public HistoryRewindUnavailableReason? UnavailableReason { get; set; } -} + /// Whether the requested mode was applied to the session. False only when an 'expectedMode' precondition did not hold, in which case any model change reported alongside it was still applied. + [JsonPropertyName("modeApplied")] + public bool? ModeApplied { get; set; } -/// Parameters for computing a workspace diff. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesDiffRequest -{ - /// When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. - [JsonPropertyName("ignoreWhitespace")] - public bool? IgnoreWhitespace { get; set; } + /// Whether applying the mode changed the active model. + [JsonPropertyName("modelChanged")] + public bool ModelChanged { get; set; } - /// Diff mode requested by the client. - [JsonPropertyName("mode")] - public WorkspaceDiffMode Mode { get; set; } + /// Lifecycle status of the requested mode change. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// User-facing warning produced while applying the mode change. + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Current per-window credit limit and consumption for an autopilot objective. +/// Agent interaction mode to apply to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AutopilotObjectiveCreditLimit +internal sealed class ModeSetRequest { - /// Configured AI-credit cap, when one is set. - [JsonPropertyName("credits")] - public double? Credits { get; set; } + /// Explicit response to a model-switch compaction preflight. + [JsonPropertyName("compactionDecision")] + public string? CompactionDecision { get; set; } - /// Window consumption in fractional AI credits, for display. - [JsonPropertyName("creditsUsed")] - public double CreditsUsed { get; set; } + /// Mode the session must currently be in for the change to apply. When set and the session is in a different mode the request is a no-op and reports status 'unchanged'. + [JsonPropertyName("expectedMode")] + public SessionMode? ExpectedMode { get; set; } - /// Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. - [RegularExpression("^[0-9]+$")] - [JsonPropertyName("creditsUsedNanoAiu")] - public string CreditsUsedNanoAiu { get; set; } = string.Empty; -} + /// Session whose plan-mode base state should be inherited. + [JsonPropertyName("inheritPlanBaseFromSessionId")] + public string? InheritPlanBaseFromSessionId { get; set; } -/// Public, persistence-independent projection of an autopilot objective. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AutopilotObjectiveState -{ - /// Optional summary recorded when the objective completed. - [JsonPropertyName("completionSummary")] - public string? CompletionSummary { get; set; } + /// The session mode the agent is operating in. + [JsonPropertyName("mode")] + public SessionMode Mode { get; set; } - /// Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. - [RegularExpression("^[0-9]+$")] - [JsonPropertyName("creditCountNanoAiu")] - public string CreditCountNanoAiu { get; set; } = string.Empty; + /// Whether the selected plan model should be persisted. + [JsonPropertyName("persistPlanSelection")] + public bool? PersistPlanSelection { get; set; } - /// Current per-window consumption and optional cap, when a credit-tracking window is present. - [JsonPropertyName("creditLimit")] - public AutopilotObjectiveCreditLimit? CreditLimit { get; set; } + /// Settings context used when persisting the selected plan model. + [JsonPropertyName("pickerSettingsContext")] + public ModelPickerSettingsContext? PickerSettingsContext { get; set; } - /// Session-local objective identifier. - [JsonPropertyName("id")] - public long Id { get; set; } + /// Context tier to use with the dedicated plan model. + [JsonPropertyName("planContextTier")] + public string? PlanContextTier { get; set; } - /// User-provided objective text. - [JsonPropertyName("objective")] - public string Objective { get; set; } = string.Empty; + /// Action to perform when leaving plan mode. + [JsonPropertyName("planExitAction")] + public string? PlanExitAction { get; set; } - /// Optional reason the objective is paused. - [JsonPropertyName("pauseReason")] - public string? PauseReason { get; set; } + /// Dedicated model to use in plan mode, when configured. + [JsonPropertyName("planModel")] + public string? PlanModel { get; set; } - /// Current normalized lifecycle status. - [JsonPropertyName("status")] - public AutopilotObjectiveStatus Status { get; set; } + /// Whether a dedicated plan model is configured. + [JsonPropertyName("planModelConfigured")] + public bool? PlanModelConfigured { get; set; } - /// Number of objective turns started. - [JsonPropertyName("turnCount")] - public long TurnCount { get; set; } -} + /// Reasoning effort to use with the dedicated plan model. + [JsonPropertyName("planReasoningEffort")] + public string? PlanReasoningEffort { get; set; } -/// Canonical runtime state for the session's current autopilot objective. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AutopilotObjectiveGetStateResult -{ - /// Current objective state, or `null` when the session has no objective. - [JsonPropertyName("state")] - public AutopilotObjectiveState? State { get; set; } -} + /// Whether leaving plan mode should restore the session's previous model. + [JsonPropertyName("restorePlanModel")] + public bool? RestorePlanModel { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAutopilotObjectiveGetStateRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Characters that, when typed in the composer, should trigger a `completions.request`. Empty when the session has no host-driven completions (e.g. local sessions, or a relay host that does not advertise `completionTriggerCharacters`). +/// The session's friendly name, or null when not yet set. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CompletionsGetTriggerCharactersResult +public sealed class NameGetResult { - /// Trigger characters advertised by the host (e.g. `["@", "#"]`). Empty disables host-driven completions for the session. - [JsonPropertyName("triggerCharacters")] - public IList TriggerCharacters { get => field ??= []; set; } + /// The session name (user-set or auto-generated), or null if not yet set. + [JsonPropertyName("name")] + public string? Name { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCompletionsGetTriggerCharactersRequest +internal sealed class SessionNameGetRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single host-driven completion. Accepting an item replaces `[rangeStart, rangeEnd)` (UTF-16 code units) in the composer with `insertText`; when the range is absent, the active token around the cursor is replaced. +/// New friendly name to apply to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionCompletionItem +internal sealed class NameSetRequest { - /// Text spliced into the composer when the item is accepted. - [JsonPropertyName("insertText")] - public string InsertText { get; set; } = string.Empty; - - /// Render-kind hint for the picker row (e.g. `"document"`, `"directory"`), derived from the host's display kind. - [JsonPropertyName("kind")] - public string? Kind { get; set; } - - /// Primary display label for the picker row. Falls back to `insertText` when absent. - [JsonPropertyName("label")] - public string? Label { get; set; } - - /// End (exclusive) of the replacement range in `text`, in UTF-16 code units. - [JsonPropertyName("rangeEnd")] - public long? RangeEnd { get; set; } + /// New session name (1–100 characters, trimmed of leading/trailing whitespace). + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(100)] + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Start of the replacement range in `text`, in UTF-16 code units. - [JsonPropertyName("rangeStart")] - public long? RangeStart { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. +/// Indicates whether the auto-generated summary was applied as the session's name. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CompletionsRequestResult +public sealed class NameSetAutoResult { - /// Completion items in host-ranked order. - [JsonPropertyName("items")] - public IList Items { get => field ??= []; set; } + /// Whether the auto-generated summary was persisted. False if the session already has a user-set name, the summary normalized to empty, or the session does not have a workspace. + [JsonPropertyName("applied")] + public bool Applied { get; set; } } -/// Request host-driven completions for the current composer input. +/// Auto-generated session summary to apply as the session's name when no user-set name exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CompletionsRequestRequest +internal sealed class NameSetAutoRequest { - /// Cursor offset within `text`, in UTF-16 code units. - [JsonPropertyName("offset")] - public long Offset { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// The full composed composer input. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// Auto-generated session summary. Empty/whitespace-only values are ignored; values are trimmed before persisting. + [JsonPropertyName("summary")] + public string Summary { get; set; } = string.Empty; } -/// Instruction sources loaded for the session, in merge order. +/// Existence, contents, and resolved path of the session plan file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstructionsGetSourcesResult +public sealed class PlanReadResult { - /// Instruction sources for the session. - [JsonPropertyName("sources")] - public IList Sources { get => field ??= []; set; } + /// The content of the plan file, or null if it does not exist. + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Whether the plan file exists in the workspace. + [JsonPropertyName("exists")] + public bool Exists { get; set; } + + /// Absolute file path of the plan file, or null if workspace is not enabled. + [JsonPropertyName("path")] + public string? Path { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionInstructionsGetSourcesRequest +internal sealed class SessionPlanReadRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Replacement contents to write to the session plan file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionInstructionsReloadRequest +internal sealed class PlanUpdateRequest { + /// The new content for the plan file. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillsLoadDiagnostics -{ - /// Errors emitted while loading skills (e.g. skills that failed to load entirely). - [JsonPropertyName("errors")] - public IList Errors { get => field ??= []; set; } - - /// Warnings emitted while loading skills (e.g. skills that loaded but had issues). - [JsonPropertyName("warnings")] - public IList Warnings { get => field ??= []; set; } -} - /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCustomizationsReloadRequest +internal sealed class SessionPlanDeleteRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether fleet mode was successfully activated. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FleetStartResult -{ - /// Whether fleet mode was successfully activated. - [JsonPropertyName("started")] - public bool Started { get; set; } -} - -/// Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. +/// A single todo row read from the session SQL `todos` table. All fields are optional because the SQL schema is best-effort and the agent may not have populated every column. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FleetStartRequest +public sealed class PlanSqlTodosRow { - /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request. - [JsonPropertyName("attachments")] - public IList? Attachments { get; set; } + /// Todo creation time, as stored by the session SQL schema's `datetime('now')` default: `YYYY-MM-DD HH:MM:SS` in UTC. Lets clients attribute todos to the work item that created them (e.g. scoping a goal's progress to the todos it produced) rather than to the whole session. + [JsonPropertyName("createdAt")] + public string? CreatedAt { get; set; } - /// If false, this request will not trigger a Premium Request Unit charge. User requests default to billable. - [JsonInclude] - [JsonPropertyName("billable")] - internal bool? Billable { get; set; } + /// Todo description. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Optional user prompt to combine with fleet instructions. - [JsonPropertyName("prompt")] - public string? Prompt { get; set; } + /// Todo identifier. + [JsonPropertyName("id")] + public string? Id { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Todo status. + [JsonPropertyName("status")] + public string? Status { get; set; } - /// If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. - [JsonPropertyName("wait")] - public bool? Wait { get; set; } + /// Todo title. + [JsonPropertyName("title")] + public string? Title { get; set; } } -/// Agents available to the session. +/// Todo rows read from the session SQL database. Empty when no session database is available. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentList +public sealed class PlanReadSqlTodosResult { - /// Available agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } + /// Rows from the session SQL todos table, ordered by creation time with insertion order used to break ties when available and id used for WITHOUT ROWID tables. + [JsonPropertyName("rows")] + public IList Rows { get => field ??= []; set; } } -/// RPC data type for SessionAgentList operations. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionAgentListRequest +internal sealed class SessionPlanReadSqlTodosRequest { - /// When true, request the session's configured built-in agents alongside custom agents. Listing applies feature, context, inclusion, exclusion, and user-disabled-agent policy, but does not evaluate transient invocation requirements such as model availability. Built-in metadata may be omitted when the session cannot project it, such as a relay session. - [JsonPropertyName("includeBuiltInAgents")] - public bool? IncludeBuiltInAgents { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// When true, request authored base prompt text on each AgentInfo. Prompt text may be omitted when unavailable, such as for agents projected through a relay session. - [JsonPropertyName("includePrompt")] - public bool? IncludePrompt { get; set; } +/// A single dependency edge read from the session SQL `todo_deps` table, indicating that one todo must complete before another. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PlanSqlTodoDependency +{ + /// ID of the todo it depends on. + [JsonPropertyName("dependsOn")] + public string DependsOn { get; set; } = string.Empty; + + /// ID of the todo that has the dependency. + [JsonPropertyName("todoId")] + public string TodoId { get; set; } = string.Empty; } -/// RPC data type for SessionAgentListRequestWithSession operations. +/// Todo rows + dependency edges read from the session SQL database. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentListRequestWithSession +public sealed class PlanReadSqlTodosWithDependenciesResult { - /// When true, request the session's configured built-in agents alongside custom agents. Listing applies feature, context, inclusion, exclusion, and user-disabled-agent policy, but does not evaluate transient invocation requirements such as model availability. Built-in metadata may be omitted when the session cannot project it, such as a relay session. - [JsonPropertyName("includeBuiltInAgents")] - public bool? IncludeBuiltInAgents { get; set; } + /// Edges from the session SQL todo_deps table. Empty when no database, no todo_deps table, or the SELECT failed. Read independently from `rows`, so a broken todo_deps table does not affect the rows result and vice versa. + [JsonPropertyName("dependencies")] + public IList Dependencies { get => field ??= []; set; } - /// When true, request authored base prompt text on each AgentInfo. Prompt text may be omitted when unavailable, such as for agents projected through a relay session. - [JsonPropertyName("includePrompt")] - public bool? IncludePrompt { get; set; } + /// Rows from the session SQL todos table, ordered by creation time with insertion order used to break ties when available and id used for WITHOUT ROWID tables. Empty when no database, no todos table, or the SELECT failed. + [JsonPropertyName("rows")] + public IList Rows { get => field ??= []; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionPlanReadSqlTodosWithDependenciesRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// An in-memory authored prompt override for an available agent. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentSetPromptRequest +/// RPC data type for WorkspacesGetWorkspaceResultWorkspace operations. +public sealed class WorkspacesGetWorkspaceResultWorkspace { - /// Stable effective agent id. Plugin namespace separators are normalized. + /// Current Git branch. + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + /// Whether the per-session Chronicle upgrade prompt was dismissed for the workspace. + [JsonPropertyName("chronicle_sync_dismissed")] + public bool? ChronicleSyncDismissed { get; set; } + + /// Name of the client that created the workspace. + [JsonPropertyName("client_name")] + public string? ClientName { get; set; } + + /// Timestamp when the workspace was created. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Current working directory associated with the workspace. + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } + + /// Git repository root associated with the workspace. + [JsonPropertyName("git_root")] + public string? GitRoot { get; set; } + + /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. + [JsonPropertyName("host_type")] + public WorkspacesWorkspaceDetailsHostType? HostType { get; set; } + + /// Stable workspace identifier. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Replacement authored prompt. Empty text is valid. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Most recent Mission Control event identifier observed for the workspace. + [JsonPropertyName("mc_last_event_id")] + public string? McLastEventId { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Mission Control session identifier associated with the workspace. + [JsonPropertyName("mc_session_id")] + public string? McSessionId { get; set; } + + /// Mission Control task identifier associated with the workspace. + [JsonPropertyName("mc_task_id")] + public string? McTaskId { get; set; } + + /// Workspace display name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Whether the workspace session can be steered remotely. + [JsonPropertyName("remote_steerable")] + public bool? RemoteSteerable { get; set; } + + /// Repository identifier associated with the workspace. + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// Number of persisted summaries in the workspace. + [JsonPropertyName("summary_count")] + public long? SummaryCount { get; set; } + + /// Timestamp when the workspace was last updated. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Whether the workspace name was explicitly chosen by the user. + [JsonPropertyName("user_named")] + public bool? UserNamed { get; set; } } -/// The currently selected custom agent, or null when using the default agent. +/// Current workspace metadata for the session, including its absolute filesystem path when available. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentGetCurrentResult +public sealed class WorkspacesGetWorkspaceResult { - /// Currently selected custom agent, or null if using the default agent. - [JsonPropertyName("agent")] - public AgentInfo? Agent { get; set; } + /// Absolute filesystem path to the workspace directory. Omitted when the session has no workspace (e.g. remote sessions). + [JsonPropertyName("path")] + public string? Path { get; set; } + + /// Current workspace metadata, or null if not available. + [JsonPropertyName("workspace")] + public WorkspacesGetWorkspaceResultWorkspace? Workspace { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentGetCurrentRequest +internal sealed class SessionWorkspacesGetWorkspaceRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The newly selected custom agent. +/// Workspace metadata fields to update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentSelectResult +internal sealed class WorkspacesUpdateMetadataRequest { - /// The newly selected custom agent. - [JsonPropertyName("agent")] - public AgentInfo Agent { get => field ??= new(); set; } -} + /// Opaque workspace context supplied by the session host. + [JsonPropertyName("context")] + public JsonElement? Context { get; set; } -/// Name of the custom agent to select for subsequent turns. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentSelectRequest -{ - /// Name of the custom agent to select. + /// Optional workspace display name override. [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + public string? Name { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Optional session context used when creating a local workspace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentDeselectRequest +internal sealed class WorkspacesEnsureRequest { + /// Opaque workspace context supplied by the session host. + [JsonPropertyName("context")] + public JsonElement? Context { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Custom agents available to the session after reloading definitions from disk. +/// Relative paths of files stored in the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentReloadResult +public sealed class WorkspacesListFilesResult { - /// Reloaded custom agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } + /// Slash-separated relative file paths in the workspace files directory. + [JsonPropertyName("files")] + public IList Files { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentReloadRequest +internal sealed class SessionWorkspacesListFilesRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifier assigned to the newly started background agent task. +/// Contents of the requested workspace file as a UTF-8 string. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksStartAgentResult +public sealed class WorkspacesReadFileResult { - /// Generated agent ID for the background task. - [JsonPropertyName("agentId")] - public string AgentId { get; set; } = string.Empty; + /// File content as a UTF-8 string. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; } -/// Agent type, prompt, name, and optional description and model override for the new task. +/// Relative path of the workspace file to read. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksStartAgentRequest +internal sealed class WorkspacesReadFileRequest { - /// Type of agent to start (e.g., 'explore', 'task', 'general-purpose'). - [JsonPropertyName("agentType")] - public string AgentType { get; set; } = string.Empty; - - /// Short description of the task. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// Optional model override. - [JsonPropertyName("model")] - public string? Model { get; set; } - - /// Friendly, non-unique name used when displaying the agent. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Task prompt for the agent. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Tracked task union returned by task APIs, containing an agent, client, or shell task. -/// Polymorphic base type discriminated by type. +/// Relative path and UTF-8 content for the workspace file to create or overwrite. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(TaskInfoAgent), "agent")] -[JsonDerivedType(typeof(TaskInfoClient), "client")] -[JsonDerivedType(typeof(TaskInfoShell), "shell")] -public partial class TaskInfo +internal sealed class WorkspacesCreateFileRequest { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} + /// File content to write as a UTF-8 string. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; -/// Tracked background agent task metadata, including IDs, status, timing, agent type, prompt, model, result, and latest response. -/// The agent variant of . + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Filesystem metadata for a path in the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoAgent : TaskInfo +public sealed class WorkspacesStatFileResult { - /// - [JsonIgnore] - public override string Type => "agent"; + /// Creation time in Unix epoch milliseconds. + [JsonPropertyName("birthtimeMs")] + public double BirthtimeMs { get; set; } - /// ISO 8601 timestamp when the current active period began. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("activeStartedAt")] - public DateTimeOffset? ActiveStartedAt { get; set; } + /// Whether the path identifies a directory. + [JsonPropertyName("isDirectory")] + public bool IsDirectory { get; set; } - /// Accumulated active execution time in milliseconds. - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("activeTimeMs")] - public TimeSpan? ActiveTime { get; set; } + /// Whether the path identifies a regular file. + [JsonPropertyName("isFile")] + public bool IsFile { get; set; } - /// Type of agent running this task. - [JsonPropertyName("agentType")] - public required string AgentType { get; set; } + /// Last modification time in Unix epoch milliseconds. + [JsonPropertyName("mtimeMs")] + public double MtimeMs { get; set; } - /// Whether the task is currently in the original sync wait and can be moved to background mode. False once it is already backgrounded, idle, finished, or no longer has a promotable sync waiter. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("canPromoteToBackground")] - public bool? CanPromoteToBackground { get; set; } + /// Size in bytes. + [JsonPropertyName("size")] + public double Size { get; set; } +} - /// ISO 8601 timestamp when the task finished. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } +/// Relative path of the workspace file or directory to inspect. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesStatFileRequest +{ + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Short description of the task. - [JsonPropertyName("description")] - public required string Description { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Friendly, non-unique name intended for display. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } +/// Directory to create within the session workspace files directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesCreateDirectoryRequest +{ + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Error message when the task failed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Whether to create missing parent directories. Defaults to false. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } - /// Whether task execution is synchronously awaited or managed in the background. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("executionMode")] - public TaskExecutionMode? ExecutionMode { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Unique task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } +/// File or directory to remove from the session workspace files directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesRemovePathRequest +{ + /// Whether a missing path should be treated as success. Defaults to false. + [JsonPropertyName("force")] + public bool? Force { get; set; } - /// ISO 8601 timestamp when the agent entered idle state. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Most recent response text from the agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("latestResponse")] - public string? LatestResponse { get; set; } + /// Whether to remove directory contents recursively. Defaults to false. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } - /// Requested model override for the task when specified. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Most recent prompt delivered to the agent. Updated whenever the agent receives a follow-up message. - [JsonPropertyName("prompt")] - public required string Prompt { get; set; } +/// Source and destination paths for a rename within the session workspace files directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesRenamePathRequest +{ + /// Slash-separated destination path relative to the workspace files directory. + [JsonPropertyName("destination")] + public string Destination { get; set; } = string.Empty; - /// Runtime model resolved for the task when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("resolvedModel")] - public string? ResolvedModel { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Result text from the task when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public string? Result { get; set; } + /// Slash-separated source path relative to the workspace files directory. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; +} - /// ISO 8601 timestamp when the task was started. - [JsonPropertyName("startedAt")] - public required DateTimeOffset StartedAt { get; set; } +/// Workspace checkpoint metadata with assigned number, human-readable title, and checkpoint filename. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesCheckpoints +{ + /// Filename of the checkpoint within the workspace checkpoints directory. + [JsonPropertyName("filename")] + public string Filename { get; set; } = string.Empty; - /// Current lifecycle status of the task. - [JsonPropertyName("status")] - public required TaskStatus Status { get; set; } + /// Checkpoint number assigned by the workspace manager. + [JsonPropertyName("number")] + public long Number { get; set; } - /// Tool call ID associated with this agent task. - [JsonPropertyName("toolCallId")] - public required string ToolCallId { get; set; } + /// Human-readable checkpoint title. + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; } -/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. +/// Workspace checkpoints in chronological order; empty when the workspace is not enabled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskClientOwner +public sealed class WorkspacesListCheckpointsResult { - /// ISO 8601 timestamp when the bound join disconnected. - [JsonPropertyName("disconnectedAt")] - public DateTimeOffset? DisconnectedAt { get; set; } + /// Workspace checkpoints in chronological order. Empty when workspace is not enabled. + [JsonPropertyName("checkpoints")] + public IList Checkpoints { get => field ??= []; set; } +} - /// Display-only owner name. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionWorkspacesListCheckpointsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Opaque identity of the currently or most recently bound session join. - [JsonPropertyName("joinId")] - public string JoinId { get; set; } = string.Empty; +/// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesReadCheckpointResult +{ + /// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. + [JsonPropertyName("content")] + public string? Content { get; set; } +} - /// Class of the task owner. - [JsonPropertyName("kind")] - public TaskClientOwnerKind Kind { get; set; } +/// Checkpoint number to read. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesReadCheckpointRequest +{ + /// Checkpoint number to read. + [JsonPropertyName("number")] + public long Number { get; set; } - /// Opaque session-scoped participant identity. - [JsonPropertyName("participantId")] - public string ParticipantId { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether this task's bound join is currently connected. - [JsonPropertyName("presence")] - public TaskClientOwnerPresence Presence { get; set; } +/// Metadata for the persisted summary. +public sealed class WorkspacesAddSummaryResultSummary +{ +} - /// Display-only owner source. - [JsonPropertyName("source")] - public string? Source { get; set; } +/// Refreshed metadata for the containing workspace. +public sealed class WorkspacesAddSummaryResultWorkspace +{ } -/// Tracked client-owned task metadata. -/// The client variant of . +/// Persisted summary metadata and refreshed workspace metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoClient : TaskInfo +public sealed class WorkspacesAddSummaryResult { - /// - [JsonIgnore] - public override string Type => "client"; + /// Metadata for the persisted summary. + [JsonPropertyName("summary")] + public WorkspacesAddSummaryResultSummary? Summary { get; set; } - /// ISO 8601 timestamp when the current active segment started. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("activeStartedAt")] - public DateTimeOffset? ActiveStartedAt { get; set; } + /// Refreshed metadata for the containing workspace. + [JsonPropertyName("workspace")] + public WorkspacesAddSummaryResultWorkspace? Workspace { get; set; } +} - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeTimeMs")] - public required long ActiveTimeMs { get; set; } +/// Compaction summary checkpoint to persist. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesAddSummaryRequest +{ + /// Markdown summary content to persist. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Whether the currently bound owner can receive a cancellation request. - [JsonPropertyName("canCancel")] - public required bool CanCancel { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Human-readable reason for terminal cancellation. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cancellationReason")] - public string? CancellationReason { get; set; } + /// Summary title shown in checkpoint listings. + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; +} - /// Owner-scoped registration and reclaim key. - [JsonPropertyName("clientTaskId")] - public required string ClientTaskId { get; set; } +/// Rollback point for local workspace summaries. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesTruncateSummariesRequest +{ + /// Number of newest summaries to keep. + [JsonPropertyName("keepCount")] + public long KeepCount { get; set; } - /// ISO 8601 timestamp when the task reached a terminal status. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Task description. - [JsonPropertyName("description")] - public required string Description { get; set; } +/// Autopilot objective file content, or null when missing. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesReadAutopilotObjectiveResult +{ + /// Autopilot objective file content, or null when missing. + [JsonPropertyName("content")] + public string? Content { get; set; } +} - /// Optional task display name. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionWorkspacesReadAutopilotObjectiveRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Human-readable terminal failure message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("error")] - public string? Error { get; set; } +/// Result of writing the autopilot objective file. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesWriteAutopilotObjectiveResult +{ + /// Filesystem operation performed. + [JsonPropertyName("operation")] + public string Operation { get; set; } = string.Empty; +} - /// Optional owner-supplied terminal failure code. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("errorCode")] - public string? ErrorCode { get; set; } +/// Autopilot objective file content to persist. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesWriteAutopilotObjectiveRequest +{ + /// Autopilot objective file content. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Execution mode, which is always background for client-owned tasks. - [JsonPropertyName("executionMode")] - public required TaskClientExecutionMode ExecutionMode { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Canonical runtime-generated task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } +/// Result of deleting the autopilot objective file. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesDeleteAutopilotObjectiveResult +{ + /// True when a file was deleted. + [JsonPropertyName("deleted")] + public bool Deleted { get; set; } +} - /// ISO 8601 timestamp when the connected owner entered idle status. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionWorkspacesDeleteAutopilotObjectiveRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// ISO 8601 timestamp of the most recent orphan transition. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("orphanedAt")] - public DateTimeOffset? OrphanedAt { get; set; } +/// Whether the autopilot objective file exists. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesAutopilotObjectiveExistsResult +{ + /// True when the objective file exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } +} - /// Public attribution and presence for the task owner. - [JsonPropertyName("owner")] - public required TaskClientOwner Owner { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionWorkspacesAutopilotObjectiveExistsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// ISO 8601 timestamp of the most recent successful reclaim. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reclaimedAt")] - public DateTimeOffset? ReclaimedAt { get; set; } +/// RPC data type for WorkspacesSaveLargePasteResultSaved operations. +public sealed class WorkspacesSaveLargePasteResultSaved +{ + /// Filename within the workspace files directory. + [JsonPropertyName("filename")] + public string Filename { get; set; } = string.Empty; - /// Opaque successful terminal result supplied by the task owner. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Absolute filesystem path to the saved paste file. + [JsonPropertyName("filePath")] + public string FilePath { get; set; } = string.Empty; - /// Sequence number of the latest accepted owner update. - [JsonPropertyName("sequence")] - public required long Sequence { get; set; } + /// Size of the saved file in bytes. + [JsonPropertyName("sizeBytes")] + public long SizeBytes { get; set; } +} - /// ISO 8601 timestamp when the task started. - [JsonPropertyName("startedAt")] - public required DateTimeOffset StartedAt { get; set; } +/// Descriptor for the saved paste file, or null when the workspace is unavailable. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesSaveLargePasteResult +{ + /// Saved-paste descriptor, or null when the workspace is unavailable (e.g. CCA runtime, non-infinite sessions, remote sessions). + [JsonPropertyName("saved")] + public WorkspacesSaveLargePasteResultSaved? Saved { get; set; } +} - /// Client task lifecycle status. - [JsonPropertyName("status")] - public required TaskClientStatus Status { get; set; } +/// Pasted content to save as a UTF-8 file in the session workspace. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkspacesSaveLargePasteRequest +{ + /// Pasted content to save as a UTF-8 file. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// ISO 8601 timestamp of the latest accepted lifecycle change. - [JsonPropertyName("updatedAt")] - public required DateTimeOffset UpdatedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. -/// The shell variant of . +/// A single changed file and its unified diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoShell : TaskInfo +public sealed class WorkspaceDiffFileChange { - /// - [JsonIgnore] - public override string Type => "shell"; - - /// Whether the shell runs inside a managed PTY session or as an independent background process. - [JsonPropertyName("attachmentMode")] - public required TaskShellInfoAttachmentMode AttachmentMode { get; set; } + /// Type of change represented by this file diff. + [JsonPropertyName("changeType")] + public WorkspaceDiffFileChangeType ChangeType { get; set; } - /// Whether this shell task can be promoted to background mode. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("canPromoteToBackground")] - public bool? CanPromoteToBackground { get; set; } + /// Unified diff content for the file. Empty when the diff was truncated. + [JsonPropertyName("diff")] + public string Diff { get; set; } = string.Empty; - /// Command being executed. - [JsonPropertyName("command")] - public required string Command { get; set; } + /// Whether the diff content was omitted because it exceeded the per-file size limit. + [JsonPropertyName("isTruncated")] + public bool? IsTruncated { get; set; } - /// ISO 8601 timestamp when the task finished. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } + /// Original file path for renamed files. + [JsonPropertyName("oldPath")] + public string? OldPath { get; set; } - /// Short description of the task. - [JsonPropertyName("description")] - public required string Description { get; set; } + /// Path to the changed file, relative to the workspace root when the file lives under it. A file changed outside the workspace root keeps a `../`-relative path, or an absolute path when no relative path exists (for example a different Windows drive). + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} - /// Whether task execution is synchronously awaited or managed in the background. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("executionMode")] - public TaskExecutionMode? ExecutionMode { get; set; } +/// Workspace diff result for the requested mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspaceDiffResult +{ + /// Default branch used for a branch diff, when branch mode was requested. + [JsonPropertyName("baseBranch")] + public string? BaseBranch { get; set; } - /// Unique task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } + /// Changed files and their unified diffs. + [JsonPropertyName("changes")] + public IList Changes { get => field ??= []; set; } - /// Path to the detached shell log, when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("logPath")] - public string? LogPath { get; set; } + /// Whether the requested diff fell back to unstaged changes, either because branch diff failed or session diff was unavailable. + [JsonPropertyName("isFallback")] + public bool IsFallback { get; set; } - /// Process ID when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pid")] - public long? Pid { get; set; } + /// Effective mode used for the returned changes. + [JsonPropertyName("mode")] + public WorkspaceDiffMode Mode { get; set; } - /// ISO 8601 timestamp when the task was started. - [JsonPropertyName("startedAt")] - public required DateTimeOffset StartedAt { get; set; } + /// Diff mode requested by the client. + [JsonPropertyName("requestedMode")] + public WorkspaceDiffMode RequestedMode { get; set; } - /// Current lifecycle status of the task. - [JsonPropertyName("status")] - public required TaskStatus Status { get; set; } + /// Why the session diff could not be produced, when applicable. Set only when `session` mode was requested and `isFallback` is true, so a client can tell the permanent `file-change-tracking-disabled` apart from the transient `session-busy`, which the same request answers once the session settles. Never set for `unstaged` or `branch` mode, and never `unsupported-remote-session`: a remote session's captures live on its own host, so a `session`-mode diff is rejected for one rather than answered with a controller-side fallback. + [JsonPropertyName("unavailableReason")] + public HistoryRewindUnavailableReason? UnavailableReason { get; set; } } -/// Background tasks currently tracked by the session. +/// Parameters for computing a workspace diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskList +internal sealed class WorkspacesDiffRequest { - /// Currently tracked tasks. - [JsonPropertyName("tasks")] - public IList Tasks { get => field ??= []; set; } -} + /// When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false. + [JsonPropertyName("ignoreWhitespace")] + public bool? IgnoreWhitespace { get; set; } + + /// Diff mode requested by the client. + [JsonPropertyName("mode")] + public WorkspaceDiffMode Mode { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksListRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Tracked client-owned task metadata. +/// Current per-window credit limit and consumption for an autopilot objective. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskClientInfo +public sealed class AutopilotObjectiveCreditLimit { - /// ISO 8601 timestamp when the current active segment started. - [JsonPropertyName("activeStartedAt")] - public DateTimeOffset? ActiveStartedAt { get; set; } - - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeTimeMs")] - public long ActiveTimeMs { get; set; } - - /// Whether the currently bound owner can receive a cancellation request. - [JsonPropertyName("canCancel")] - public bool CanCancel { get; set; } - - /// Human-readable reason for terminal cancellation. - [JsonPropertyName("cancellationReason")] - public string? CancellationReason { get; set; } - - /// Owner-scoped registration and reclaim key. - [JsonPropertyName("clientTaskId")] - public string ClientTaskId { get; set; } = string.Empty; - - /// ISO 8601 timestamp when the task reached a terminal status. - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } + /// Configured AI-credit cap, when one is set. + [JsonPropertyName("credits")] + public double? Credits { get; set; } - /// Task description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Window consumption in fractional AI credits, for display. + [JsonPropertyName("creditsUsed")] + public double CreditsUsed { get; set; } - /// Optional task display name. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + [RegularExpression("^[0-9]+$")] + [JsonPropertyName("creditsUsedNanoAiu")] + public string CreditsUsedNanoAiu { get; set; } = string.Empty; +} - /// Human-readable terminal failure message. - [JsonPropertyName("error")] - public string? Error { get; set; } +/// Public, persistence-independent projection of an autopilot objective. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AutopilotObjectiveState +{ + /// Optional summary recorded when the objective completed. + [JsonPropertyName("completionSummary")] + public string? CompletionSummary { get; set; } - /// Optional owner-supplied terminal failure code. - [JsonPropertyName("errorCode")] - public string? ErrorCode { get; set; } + /// Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + [RegularExpression("^[0-9]+$")] + [JsonPropertyName("creditCountNanoAiu")] + public string CreditCountNanoAiu { get; set; } = string.Empty; - /// Execution mode, which is always background for client-owned tasks. - [JsonPropertyName("executionMode")] - public TaskClientExecutionMode ExecutionMode { get; set; } + /// Current per-window consumption and optional cap, when a credit-tracking window is present. + [JsonPropertyName("creditLimit")] + public AutopilotObjectiveCreditLimit? CreditLimit { get; set; } - /// Canonical runtime-generated task identifier. + /// Session-local objective identifier. [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// ISO 8601 timestamp when the connected owner entered idle status. - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } - - /// ISO 8601 timestamp of the most recent orphan transition. - [JsonPropertyName("orphanedAt")] - public DateTimeOffset? OrphanedAt { get; set; } - - /// Public attribution and presence for the task owner. - [JsonPropertyName("owner")] - public TaskClientOwner Owner { get => field ??= new(); set; } - - /// ISO 8601 timestamp of the most recent successful reclaim. - [JsonPropertyName("reclaimedAt")] - public DateTimeOffset? ReclaimedAt { get; set; } - - /// Opaque successful terminal result supplied by the task owner. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + public long Id { get; set; } - /// Sequence number of the latest accepted owner update. - [JsonPropertyName("sequence")] - public long Sequence { get; set; } + /// User-provided objective text. + [JsonPropertyName("objective")] + public string Objective { get; set; } = string.Empty; - /// ISO 8601 timestamp when the task started. - [JsonPropertyName("startedAt")] - public DateTimeOffset StartedAt { get; set; } + /// Optional reason the objective is paused. + [JsonPropertyName("pauseReason")] + public string? PauseReason { get; set; } - /// Client task lifecycle status. + /// Current normalized lifecycle status. [JsonPropertyName("status")] - public TaskClientStatus Status { get; set; } - - /// Task kind. - [JsonPropertyName("type")] - public TaskClientType Type { get; set; } + public AutopilotObjectiveStatus Status { get; set; } - /// ISO 8601 timestamp of the latest accepted lifecycle change. - [JsonPropertyName("updatedAt")] - public DateTimeOffset UpdatedAt { get; set; } + /// Number of objective turns started. + [JsonPropertyName("turnCount")] + public long TurnCount { get; set; } } -/// Result of registering or reclaiming a client-owned task. +/// Canonical runtime state for the session's current autopilot objective. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRegisterResult +public sealed class AutopilotObjectiveGetStateResult { - /// True only when this invocation created a new task. - [JsonPropertyName("created")] - public bool Created { get; set; } - - /// True only when this invocation reclaimed an orphaned task. - [JsonPropertyName("reclaimed")] - public bool Reclaimed { get; set; } - - /// Authoritative registered or reclaimed task. - [JsonPropertyName("task")] - public TaskClientInfo Task { get => field ??= new(); set; } + /// Current objective state, or `null` when the session has no objective. + [JsonPropertyName("state")] + public AutopilotObjectiveState? State { get; set; } } -/// Registers or reclaims a client-owned task. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksRegisterRequest +internal sealed class SessionAutopilotObjectiveGetStateRequest { - /// Whether the owner supports runtime cancellation requests. - [JsonPropertyName("cancellable")] - public bool Cancellable { get; set; } - - /// Owner-scoped idempotency key used for registration and reclaim. - [JsonPropertyName("clientTaskId")] - public string ClientTaskId { get; set; } = string.Empty; - - /// Human-readable description of the external work. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// Optional short display name for the external work. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } - - /// Expected current sequence for idempotent registration or orphan reclaim. - [JsonPropertyName("expectedSequence")] - public long? ExpectedSequence { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Task kind. - [JsonPropertyName("type")] - public TaskClientType Type { get; set; } } -/// Result of publishing a client-owned task update. +/// Characters that, when typed in the composer, should trigger a `completions.request`. Empty when the session has no host-driven completions (e.g. local sessions, or a relay host that does not advertise `completionTriggerCharacters`). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksUpdateResult +public sealed class CompletionsGetTriggerCharactersResult { - /// Whether this invocation changed task state. - [JsonPropertyName("applied")] - public bool Applied { get; set; } - - /// Whether this invocation repeated the latest accepted update. - [JsonPropertyName("duplicate")] - public bool Duplicate { get; set; } - - /// Authoritative task after processing the update. - [JsonPropertyName("task")] - public TaskClientInfo Task { get => field ??= new(); set; } + /// Trigger characters advertised by the host (e.g. `["@", "#"]`). Empty disables host-driven completions for the session. + [JsonPropertyName("triggerCharacters")] + public IList TriggerCharacters { get => field ??= []; set; } } -/// Progress or terminal update for a client-owned task. -/// Polymorphic base type discriminated by kind. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(TaskClientUpdateProgress), "progress")] -[JsonDerivedType(typeof(TaskClientUpdateCompleted), "completed")] -[JsonDerivedType(typeof(TaskClientUpdateFailed), "failed")] -[JsonDerivedType(typeof(TaskClientUpdateCancelled), "cancelled")] -public partial class TaskClientUpdate +internal sealed class SessionCompletionsGetTriggerCharactersRequest { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } - -/// Publishes nonterminal progress for a running or idle client task. -/// The progress variant of . +/// A single host-driven completion. Accepting an item replaces `[rangeStart, rangeEnd)` (UTF-16 code units) in the composer with `insertText`; when the range is absent, the active token around the cursor is replaced. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateProgress : TaskClientUpdate +public sealed class SessionCompletionItem { - /// - [JsonIgnore] - public override string Kind => "progress"; - - /// Optional progress message appended to recent activity when nonempty. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } - - /// Optional completion percentage; null clears the current percentage. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("percentage")] - public double? Percentage { get; set; } - - /// Optional progress phase; null clears the current phase. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("phase")] - public string? Phase { get; set; } - - /// Optional active status transition. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("status")] - public TaskClientActiveStatus? Status { get; set; } -} - -/// Reports successful terminal completion. -/// The completed variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateCompleted : TaskClientUpdate -{ - /// - [JsonIgnore] - public override string Kind => "completed"; - - /// Optional final progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } - - /// Optional opaque successful terminal result. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } -} + /// Text spliced into the composer when the item is accepted. + [JsonPropertyName("insertText")] + public string InsertText { get; set; } = string.Empty; -/// Reports terminal failure. -/// The failed variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateFailed : TaskClientUpdate -{ - /// - [JsonIgnore] - public override string Kind => "failed"; + /// Render-kind hint for the picker row (e.g. `"document"`, `"directory"`), derived from the host's display kind. + [JsonPropertyName("kind")] + public string? Kind { get; set; } - /// Optional owner-supplied terminal failure code. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("code")] - public string? Code { get; set; } + /// Primary display label for the picker row. Falls back to `insertText` when absent. + [JsonPropertyName("label")] + public string? Label { get; set; } - /// Human-readable terminal failure message. - [JsonPropertyName("error")] - public required string Error { get; set; } + /// End (exclusive) of the replacement range in `text`, in UTF-16 code units. + [JsonPropertyName("rangeEnd")] + public long? RangeEnd { get; set; } - /// Optional final progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Start of the replacement range in `text`, in UTF-16 code units. + [JsonPropertyName("rangeStart")] + public long? RangeStart { get; set; } } -/// Reports terminal cancellation after external work stopped. -/// The cancelled variant of . +/// Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateCancelled : TaskClientUpdate +public sealed class CompletionsRequestResult { - /// - [JsonIgnore] - public override string Kind => "cancelled"; - - /// Optional final progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } - - /// Optional human-readable cancellation reason. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Completion items in host-ranked order. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } } -/// Updates a client-owned task. +/// Request host-driven completions for the current composer input. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksUpdateRequest +internal sealed class CompletionsRequestRequest { - /// Canonical runtime-generated task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Owner update sequence to apply. - [JsonPropertyName("sequence")] - public long Sequence { get; set; } + /// Cursor offset within `text`, in UTF-16 code units. + [JsonPropertyName("offset")] + public long Offset { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Progress or terminal update payload. - [JsonPropertyName("update")] - public TaskClientUpdate Update { get => field ??= new(); set; } + /// The full composed composer input. + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; } -/// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. +/// Instruction sources loaded for the session, in merge order. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRefreshResult +public sealed class InstructionsGetSourcesResult { + /// Instruction sources for the session. + [JsonPropertyName("sources")] + public IList Sources { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksRefreshRequest +internal sealed class SessionInstructionsGetSourcesRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksWaitForPendingResult -{ -} - /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksWaitForPendingRequest +internal sealed class SessionInstructionsReloadRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. -/// Polymorphic base type discriminated by type. +/// Result of one customization reload component. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(TaskProgressAgent), "agent")] -[JsonDerivedType(typeof(TaskProgressClient), "client")] -[JsonDerivedType(typeof(TaskProgressShell), "shell")] -public partial class TaskProgress +public sealed class CustomizationReloadOutcome { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} - + /// Reason for a skipped component or description of a failure, when available. + [JsonPropertyName("detail")] + public string? Detail { get; set; } -/// Timestamped display line for task progress output or recent agent activity. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskProgressLine -{ - /// Display message, e.g., "▸ bash", "✓ edit src/foo.ts". - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Whether the component reloaded, was skipped, or failed. + [JsonPropertyName("status")] + public CustomizationReloadStatus Status { get; set; } - /// ISO 8601 timestamp when this event occurred. - [JsonPropertyName("timestamp")] - public DateTimeOffset Timestamp { get; set; } + /// Component whose reload was attempted or skipped. + [JsonPropertyName("subsystem")] + public CustomizationReloadSubsystem Subsystem { get; set; } } -/// Progress snapshot for an agent task, with recent activity lines and optional latest intent. -/// The agent variant of . +/// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressAgent : TaskProgress +public sealed class CustomizationsReloadResult { - /// - [JsonIgnore] - public override string Type => "agent"; + /// Errors from any component that could not be refreshed. + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } - /// The most recent intent reported by the agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("latestIntent")] - public string? LatestIntent { get; set; } + /// Outcome of each component in reload order; a skipped component was not configured or loaded. + [JsonPropertyName("outcomes")] + public IList Outcomes { get => field ??= []; set; } - /// Recent tool execution events converted to display lines. - [JsonPropertyName("recentActivity")] - public required IList RecentActivity { get; set; } + /// Warnings from skill discovery. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// Generic progress for a client-owned task. -/// The client variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressClient : TaskProgress +internal sealed class SessionCustomizationsReloadRequest { - /// - [JsonIgnore] - public override string Type => "client"; - - /// Most recent nonempty progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("lastMessage")] - public string? LastMessage { get; set; } - - /// Current completion percentage from zero through one hundred. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("percentage")] - public double? Percentage { get; set; } - - /// Current owner-defined progress phase. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("phase")] - public string? Phase { get; set; } - - /// Recent server-timestamped progress messages. - [JsonPropertyName("recentActivity")] - public required IList RecentActivity { get; set; } - - /// Sequence number of the latest accepted owner update. - [JsonPropertyName("sequence")] - public required long Sequence { get; set; } - - /// Current client task lifecycle status. - [JsonPropertyName("status")] - public required TaskClientStatus Status { get; set; } - - /// ISO 8601 timestamp of the latest accepted lifecycle change. - [JsonPropertyName("updatedAt")] - public required DateTimeOffset UpdatedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. -/// The shell variant of . +/// Indicates whether fleet mode was successfully activated. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressShell : TaskProgress +public sealed class FleetStartResult { - /// - [JsonIgnore] - public override string Type => "shell"; - - /// Process ID when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pid")] - public long? Pid { get; set; } - - /// Recent stdout/stderr lines from the running shell command. - [JsonPropertyName("recentOutput")] - public required string RecentOutput { get; set; } + /// Whether fleet mode was successfully activated. + [JsonPropertyName("started")] + public bool Started { get; set; } } -/// Progress information for the task, or null when no task with that ID is tracked. +/// Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksGetProgressResult +internal sealed class FleetStartRequest { - /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. - [JsonPropertyName("progress")] - public TaskProgress? Progress { get; set; } -} + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } -/// Identifier of the background task to fetch progress for. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksGetProgressRequest -{ - /// Task identifier (agent ID or shell ID). - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// If false, this request will not trigger a Premium Request Unit charge. User requests default to billable. + [JsonInclude] + [JsonPropertyName("billable")] + internal bool? Billable { get; set; } + + /// Optional user prompt to combine with fleet instructions. + [JsonPropertyName("prompt")] + public string? Prompt { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// If true, await completion of the agentic loop for this fleet request before returning. Defaults to false. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } } -/// The first sync-waiting task that can currently be promoted to background mode. +/// Agents available to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksGetCurrentPromotableResult +public sealed class AgentList { - /// The first sync-waiting task (agent first, then shell) that can currently be promoted to background mode. Omitted if no such task exists. The returned task is guaranteed to have executionMode='sync' and canPromoteToBackground=true at the time of the call. - [JsonPropertyName("task")] - public TaskInfo? Task { get; set; } + /// Available agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } } -/// Identifies the target session. +/// RPC data type for SessionAgentList operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksGetCurrentPromotableRequest +public sealed class SessionAgentListRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// When true, request the session's configured built-in agents alongside custom agents. Listing applies feature, context, inclusion, exclusion, and user-disabled-agent policy, but does not evaluate transient invocation requirements such as model availability. Built-in metadata may be omitted when the session cannot project it, such as a relay session. + [JsonPropertyName("includeBuiltInAgents")] + public bool? IncludeBuiltInAgents { get; set; } + + /// When true, request authored base prompt text on each AgentInfo. Prompt text may be omitted when unavailable, such as for agents projected through a relay session. + [JsonPropertyName("includePrompt")] + public bool? IncludePrompt { get; set; } } -/// Indicates whether the task was successfully promoted to background mode. +/// RPC data type for SessionAgentListRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksPromoteToBackgroundResult +internal sealed class SessionAgentListRequestWithSession { - /// Whether the task was successfully promoted to background mode. - [JsonPropertyName("promoted")] - public bool Promoted { get; set; } + /// When true, request the session's configured built-in agents alongside custom agents. Listing applies feature, context, inclusion, exclusion, and user-disabled-agent policy, but does not evaluate transient invocation requirements such as model availability. Built-in metadata may be omitted when the session cannot project it, such as a relay session. + [JsonPropertyName("includeBuiltInAgents")] + public bool? IncludeBuiltInAgents { get; set; } + + /// When true, request authored base prompt text on each AgentInfo. Prompt text may be omitted when unavailable, such as for agents projected through a relay session. + [JsonPropertyName("includePrompt")] + public bool? IncludePrompt { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Identifier of the task to promote to background mode. +/// An in-memory authored prompt override for an available agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksPromoteToBackgroundRequest +internal sealed class AgentSetPromptRequest { - /// Task identifier. + /// Stable effective agent id. Plugin namespace separators are normalized. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + /// Replacement authored prompt. Empty text is valid. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. +/// The currently selected custom agent, or null when using the default agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksPromoteCurrentToBackgroundResult +public sealed class AgentGetCurrentResult { - /// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. Atomic operation: avoids the race window of getCurrentPromotable + promoteToBackground. - [JsonPropertyName("task")] - public TaskInfo? Task { get; set; } + /// Currently selected custom agent, or null if using the default agent. + [JsonPropertyName("agent")] + public AgentInfo? Agent { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksPromoteCurrentToBackgroundRequest +internal sealed class SessionAgentGetCurrentRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the background task was successfully cancelled. +/// The newly selected custom agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksCancelResult +public sealed class AgentSelectResult { - /// Whether the task was successfully cancelled. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// The newly selected custom agent. + [JsonPropertyName("agent")] + public AgentInfo Agent { get => field ??= new(); set; } } -/// Identifier of the background task to cancel. +/// Name of the custom agent to select for subsequent turns. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksCancelRequest +internal sealed class AgentSelectRequest { - /// Task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Name of the custom agent to select. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the task was removed. False when the task does not exist or is still running/idle. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRemoveResult -{ - /// Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). - [JsonPropertyName("removed")] - public bool Removed { get; set; } -} - -/// Identifier of the completed or cancelled task to remove from tracking. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksRemoveRequest +internal sealed class SessionAgentDeselectRequest { - /// Task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the message was delivered, with an error message when delivery failed. +/// Custom agents available to the session after reloading definitions from disk. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksSendMessageResult +public sealed class AgentReloadResult { - /// Error message if delivery failed. - [JsonPropertyName("error")] - public string? Error { get; set; } - - /// Whether the message was successfully delivered or steered. - [JsonPropertyName("sent")] - public bool Sent { get; set; } + /// Reloaded custom agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } } -/// Identifier of the target agent task, message content, and optional sender agent ID. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksSendMessageRequest +internal sealed class SessionAgentReloadRequest { - /// Agent ID of the sender, if sent on behalf of another agent. - [JsonPropertyName("fromAgentId")] - public string? FromAgentId { get; set; } - - /// Agent task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Message content to send to the agent. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Skill metadata available to a session, with name, description, source, enabled/invocable state, path, plugin, and argument hint. +/// Identifier assigned to the newly started background agent task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Skill +public sealed class TasksStartAgentResult { - /// Optional freeform hint describing the skill's expected arguments, from the `argument-hint` frontmatter field. - [JsonPropertyName("argumentHint")] - public string? ArgumentHint { get; set; } + /// Generated agent ID for the background task. + [JsonPropertyName("agentId")] + public string AgentId { get; set; } = string.Empty; +} - /// Canonical slash command name used to invoke the skill, without the leading '/'. - [JsonPropertyName("commandName")] - public string? CommandName { get; set; } +/// Agent type, prompt, name, and optional description and model override for the new task. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksStartAgentRequest +{ + /// Type of agent to start (e.g., 'explore', 'task', 'general-purpose'). + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; - /// Description of what the skill does. + /// Short description of the task. [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + public string? Description { get; set; } - /// Whether the skill is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Optional model override. + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Unique identifier for the skill. + /// Friendly, non-unique name used when displaying the agent. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Absolute path to the skill file. - [JsonPropertyName("path")] - public string? Path { get; set; } - - /// Name of the plugin that provides the skill, when source is 'plugin'. - [JsonPropertyName("pluginName")] - public string? PluginName { get; set; } - - /// Source location type (e.g., project, personal-copilot, plugin, builtin). - [JsonPropertyName("source")] - public SkillSource Source { get; set; } + /// Task prompt for the agent. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Whether the skill can be invoked by the user as a slash command. - [JsonPropertyName("userInvocable")] - public bool UserInvocable { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Skills available to the session, with their enabled state. +/// Tracked task union returned by task APIs, containing an agent, client, or shell task. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillList +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(TaskInfoAgent), "agent")] +[JsonDerivedType(typeof(TaskInfoClient), "client")] +[JsonDerivedType(typeof(TaskInfoShell), "shell")] +public partial class TaskInfo { - /// Available skills. - [JsonPropertyName("skills")] - public IList Skills { get => field ??= []; set; } + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsListRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} -/// Skill invocation record with name, path, content, allowed tools, and turn number. +/// Tracked background agent task metadata, including IDs, status, timing, agent type, prompt, model, result, and latest response. +/// The agent variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillsInvokedSkill +public partial class TaskInfoAgent : TaskInfo { - /// Tools that should be auto-approved when this skill is active, captured at invocation time. - [JsonPropertyName("allowedTools")] - public IList? AllowedTools { get; set; } + /// + [JsonIgnore] + public override string Type => "agent"; - /// Full content of the skill file. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// ISO 8601 timestamp when the current active period began. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } - /// Whether model invocation was disabled when this skill was invoked. - [JsonPropertyName("disableModelInvocation")] - public bool? DisableModelInvocation { get; set; } + /// Accumulated active execution time in milliseconds. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeTimeMs")] + public TimeSpan? ActiveTime { get; set; } - /// Turn number when the skill was invoked. - [JsonPropertyName("invokedAtTurn")] - public long InvokedAtTurn { get; set; } + /// Type of agent running this task. + [JsonPropertyName("agentType")] + public required string AgentType { get; set; } - /// Unique identifier for the skill. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Whether the task is currently in the original sync wait and can be moved to background mode. False once it is already backgrounded, idle, finished, or no longer has a promotable sync waiter. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("canPromoteToBackground")] + public bool? CanPromoteToBackground { get; set; } - /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; -} + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } -/// Skills invoked during this session, ordered by invocation time (most recent last). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillsGetInvokedResult -{ - /// Skills invoked during this session, ordered by invocation time (most recent last). - [JsonPropertyName("skills")] - public IList Skills { get => field ??= []; set; } -} + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsGetInvokedRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Friendly, non-unique name intended for display. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } -/// Name of the skill to enable for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsEnableRequest -{ - /// Name of the skill to enable. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Error message when the task failed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether task execution is synchronously awaited or managed in the background. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskExecutionMode? ExecutionMode { get; set; } -/// Name of the skill to disable for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsDisableRequest -{ - /// Name of the skill to disable. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// ISO 8601 timestamp when the agent entered idle state. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsReloadRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Most recent response text from the agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("latestResponse")] + public string? LatestResponse { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsEnsureLoadedRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Requested model override for the task when specified. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } -/// Recorded MCP server connection failure. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerFailureInfo -{ - /// Failure message produced when the MCP server connection failed. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Most recent prompt delivered to the agent. Updated whenever the agent receives a follow-up message. + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } - /// epoch-ms timestamp at which the failure was recorded. - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } -} + /// Runtime model resolved for the task when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resolvedModel")] + public string? ResolvedModel { get; set; } -/// Recorded MCP server pending-auth state. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerNeedsAuthInfo -{ - /// epoch-ms timestamp at which the server signalled it needs authentication. - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } + /// Result text from the task when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public string? Result { get; set; } + + /// ISO 8601 timestamp when the task was started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } + + /// Current lifecycle status of the task. + [JsonPropertyName("status")] + public required TaskStatus Status { get; set; } + + /// Tool call ID associated with this agent task. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } } -/// Host-level state, omitted when no MCP host is initialized. +/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpHostState +public sealed class TaskClientOwner { - /// Names of currently-connected MCP clients. - [JsonPropertyName("clients")] - public IList Clients { get => field ??= []; set; } + /// ISO 8601 timestamp when the bound join disconnected. + [JsonPropertyName("disconnectedAt")] + public DateTimeOffset? DisconnectedAt { get; set; } - /// Configured servers that are explicitly disabled. - [JsonPropertyName("disabledServers")] - public IList DisabledServers { get => field ??= []; set; } + /// Display-only owner name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Map of server name to recorded connection failure. - [JsonPropertyName("failedServers")] - public IDictionary FailedServers { get => field ??= new Dictionary(); set; } + /// Opaque identity of the currently or most recently bound session join. + [JsonPropertyName("joinId")] + public string JoinId { get; set; } = string.Empty; - /// Configured servers filtered out by MCP server policy. - [JsonPropertyName("filteredServers")] - public IList FilteredServers { get => field ??= []; set; } + /// Class of the task owner. + [JsonPropertyName("kind")] + public TaskClientOwnerKind Kind { get; set; } - /// Whether third-party MCP servers are policy-enabled for this session. - [JsonPropertyName("mcp3pEnabled")] - public bool Mcp3pEnabled { get; set; } + /// Opaque session-scoped participant identity. + [JsonPropertyName("participantId")] + public string ParticipantId { get; set; } = string.Empty; - /// Map of server name to recorded pending-auth state. - [JsonPropertyName("needsAuthServers")] - public IDictionary NeedsAuthServers { get => field ??= new Dictionary(); set; } + /// Whether this task's bound join is currently connected. + [JsonPropertyName("presence")] + public TaskClientOwnerPresence Presence { get; set; } - /// Names of servers with in-flight connection attempts. - [JsonPropertyName("pendingConnections")] - public IList PendingConnections { get => field ??= []; set; } + /// Display-only owner source. + [JsonPropertyName("source")] + public string? Source { get; set; } } -/// Owned installation that a listed MCP server's live configuration came from. +/// Tracked client-owned task metadata. +/// The client variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerOwnership +public partial class TaskInfoClient : TaskInfo { - /// Stable installation identifier from the owned installation receipt. - [JsonPropertyName("installationId")] - public string InstallationId { get; set; } = string.Empty; -} + /// + [JsonIgnore] + public override string Type => "client"; -/// MCP server status entry, including config source/plugin source and any connection error. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServer -{ - /// Human-readable display name supplied by a managed server catalog. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// ISO 8601 timestamp when the current active segment started. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } - /// Error message if the server failed to connect. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public required long ActiveTimeMs { get; set; } - /// Server name (config key). - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public required bool CanCancel { get; set; } - /// Owned installation this entry's live configuration came from. Absent for manual, workspace, plugin, builtin and same-name servers, and on runtimes without owned installations. - [JsonPropertyName("owned")] - public McpServerOwnership? Owned { get; set; } + /// Human-readable reason for terminal cancellation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } - /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. - [JsonPropertyName("serverMetadata")] - public McpServerMetadata? ServerMetadata { get; set; } + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public required string ClientTaskId { get; set; } - /// Configuration source: user, workspace, plugin, builtin, or managed. - [JsonPropertyName("source")] - public McpServerSource? Source { get; set; } + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } - /// Plugin name that provided this server, when source is plugin. - [JsonPropertyName("sourcePlugin")] - public string? SourcePlugin { get; set; } + /// Task description. + [JsonPropertyName("description")] + public required string Description { get; set; } - /// Plugin version that provided this server, when source is plugin. - [JsonPropertyName("sourcePluginVersion")] - public string? SourcePluginVersion { get; set; } + /// Optional task display name. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured. - [JsonPropertyName("status")] - public McpServerStatus Status { get; set; } -} + /// Human-readable terminal failure message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } -/// MCP servers configured for the session, with their connection status and host-level state. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerList -{ - /// Host-level state, omitted when no MCP host is initialized. - [JsonPropertyName("host")] - public McpHostState? Host { get; set; } + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } - /// Configured MCP servers. - [JsonPropertyName("servers")] - public IList Servers { get => field ??= []; set; } -} + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public required TaskClientExecutionMode ExecutionMode { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpListRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } -/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpToolUi -{ - /// URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. - [JsonPropertyName("resourceUri")] - public string? ResourceUri { get; set; } + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } - /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. - [JsonPropertyName("visibility")] - public IList? Visibility { get; set; } -} + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } -/// MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpTools -{ - /// Tool description, when provided. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public required TaskClientOwner Owner { get; set; } - /// Tool name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } - /// Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` block was present without recognized fields. - [JsonPropertyName("ui")] - public McpToolUi? Ui { get; set; } -} + /// Opaque successful terminal result supplied by the task owner. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } -/// Tools exposed by the connected MCP server. Throws when the server is not connected. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpListToolsResult -{ - /// Tools exposed by the server. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } -} + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } -/// Server name whose tool list should be returned. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpListToolsRequest -{ - /// Name of the connected MCP server whose tools to list. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Client task lifecycle status. + [JsonPropertyName("status")] + public required TaskClientStatus Status { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public required DateTimeOffset UpdatedAt { get; set; } } -/// Name of the MCP server to enable for the session. +/// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. +/// The shell variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpEnableRequest +public partial class TaskInfoShell : TaskInfo { - /// Exact receipt identity for explicit owned activation in this session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// + [JsonIgnore] + public override string Type => "shell"; - /// Name of the MCP server to enable. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } -} + /// Whether the shell runs inside a managed PTY session or as an independent background process. + [JsonPropertyName("attachmentMode")] + public required TaskShellInfoAttachmentMode AttachmentMode { get; set; } -/// Name of the MCP server to enable for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpEnableRequestWithSession -{ - /// Exact receipt identity for explicit owned activation in this session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Whether this shell task can be promoted to background mode. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("canPromoteToBackground")] + public bool? CanPromoteToBackground { get; set; } - /// Name of the MCP server to enable. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Command being executed. + [JsonPropertyName("command")] + public required string Command { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } -/// Name of the MCP server to disable for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpDisableRequest -{ - /// Required for an owned installation; omission preserves only manual-server behaviour. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { get; set; } - /// Name of the MCP server to disable. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } -} + /// Whether task execution is synchronously awaited or managed in the background. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskExecutionMode? ExecutionMode { get; set; } -/// Name of the MCP server to disable for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpDisableRequestWithSession -{ - /// Required for an owned installation; omission preserves only manual-server behaviour. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } - /// Name of the MCP server to disable. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Path to the detached shell log, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("logPath")] + public string? LogPath { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Process ID when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pid")] + public long? Pid { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpReloadRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// ISO 8601 timestamp when the task was started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } + + /// Current lifecycle status of the task. + [JsonPropertyName("status")] + public required TaskStatus Status { get; set; } } -/// Result of moving in-flight MCP loading to the background. +/// Background tasks currently tracked by the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MoveMcpLoadingToBackgroundResult +public sealed class TaskList { - /// Whether an in-flight MCP load was moved to the background, releasing turns that were waiting on it. False when no MCP load was in flight or the waiting turns had already been released. - [JsonPropertyName("movedToBackground")] - public bool MovedToBackground { get; set; } + /// Currently tracked tasks. + [JsonPropertyName("tasks")] + public IList Tasks { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpMoveLoadingToBackgroundRequest +internal sealed class SessionTasksListRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// MCP server allowed by policy, with server name and optional PII-free explanatory note. +/// Tracked client-owned task metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAllowedServer +public sealed class TaskClientInfo { - /// Allowed server name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// ISO 8601 timestamp when the current active segment started. + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } - /// PII-free note explaining why the server was allowed. - [JsonPropertyName("redactedNote")] - public string? RedactedNote { get; set; } -} + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public long ActiveTimeMs { get; set; } -/// MCP server whose connection attempt failed. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpFailedServer -{ - /// The captured connection failure detail. + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public bool CanCancel { get; set; } + + /// Human-readable reason for terminal cancellation. + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } + + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Task description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Optional task display name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Human-readable terminal failure message. [JsonPropertyName("error")] public string? Error { get; set; } - /// The config key of the server that failed to connect. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; -} + /// Optional owner-supplied terminal failure code. + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } -/// MCP server filtered by policy, with name, reason, and optional redacted reason. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpFilteredServer -{ - /// Deprecated. This field is no longer populated. - [EditorBrowsable(EditorBrowsableState.Never)] -#if NET5_0_OR_GREATER - [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] -#endif - [JsonPropertyName("enterpriseName")] - public string? EnterpriseName { get; set; } + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public TaskClientExecutionMode ExecutionMode { get; set; } - /// Filtered server name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Human-readable filter reason. - [JsonPropertyName("reason")] - public string Reason { get; set; } = string.Empty; + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } - /// PII-free filter reason. - [JsonPropertyName("redactedReason")] - public string? RedactedReason { get; set; } + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } + + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public TaskClientOwner Owner { get => field ??= new(); set; } + + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } + + /// Opaque successful terminal result supplied by the task owner. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public long Sequence { get; set; } + + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public DateTimeOffset StartedAt { get; set; } + + /// Client task lifecycle status. + [JsonPropertyName("status")] + public TaskClientStatus Status { get; set; } + + /// Task kind. + [JsonPropertyName("type")] + public TaskClientType Type { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public DateTimeOffset UpdatedAt { get; set; } } -/// MCP server startup filtering result. +/// Result of registering or reclaiming a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStartServersResult +public sealed class TasksRegisterResult { - /// Non-default servers allowed by policy. - [JsonPropertyName("allowedServers")] - public IList? AllowedServers { get; set; } + /// True only when this invocation created a new task. + [JsonPropertyName("created")] + public bool Created { get; set; } - /// Servers whose connection attempt failed. - [JsonPropertyName("failedServers")] - public IList? FailedServers { get; set; } + /// True only when this invocation reclaimed an orphaned task. + [JsonPropertyName("reclaimed")] + public bool Reclaimed { get; set; } - /// Servers filtered out before startup. - [JsonPropertyName("filteredServers")] - public IList FilteredServers { get => field ??= []; set; } + /// Authoritative registered or reclaimed task. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } } -/// Opaque MCP reload configuration. +/// Registers or reclaims a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpReloadWithConfigRequest +internal sealed class TasksRegisterRequest { + /// Whether the owner supports runtime cancellation requests. + [JsonPropertyName("cancellable")] + public bool Cancellable { get; set; } + + /// Owner-scoped idempotency key used for registration and reclaim. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// Human-readable description of the external work. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Optional short display name for the external work. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Expected current sequence for idempotent registration or orphan reclaim. + [JsonPropertyName("expectedSequence")] + public long? ExpectedSequence { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; -} -/// MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpExecuteSamplingResult -{ + /// Task kind. + [JsonPropertyName("type")] + public TaskClientType Type { get; set; } } -/// Outcome of an MCP sampling execution: success result, failure error, or cancellation. +/// Result of publishing a client-owned task update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpSamplingExecutionResult +public sealed class TasksUpdateResult { - /// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. - [JsonPropertyName("action")] - public McpSamplingExecutionAction Action { get; set; } + /// Whether this invocation changed task state. + [JsonPropertyName("applied")] + public bool Applied { get; set; } - /// Error description, present when action='failure'. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Whether this invocation repeated the latest accepted update. + [JsonPropertyName("duplicate")] + public bool Duplicate { get; set; } - /// MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. - [JsonPropertyName("result")] - public McpExecuteSamplingResult? Result { get; set; } + /// Authoritative task after processing the update. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } } -/// Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. +/// Progress or terminal update for a client-owned task. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpExecuteSamplingRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(TaskClientUpdateProgress), "progress")] +[JsonDerivedType(typeof(TaskClientUpdateCompleted), "completed")] +[JsonDerivedType(typeof(TaskClientUpdateFailed), "failed")] +[JsonDerivedType(typeof(TaskClientUpdateCancelled), "cancelled")] +public partial class TaskClientUpdate { + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. + +/// Publishes nonterminal progress for a running or idle client task. +/// The progress variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpExecuteSamplingParams +public partial class TaskClientUpdateProgress : TaskClientUpdate { - /// The original MCP JSON-RPC request ID (string or number). Used by the runtime to correlate the inference with the originating MCP request for telemetry; this is distinct from `requestId` (which is the schema-level cancellation handle). - [JsonPropertyName("mcpRequestId")] - public JsonElement McpRequestId { get; set; } + /// + [JsonIgnore] + public override string Kind => "progress"; - /// Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. - [JsonPropertyName("request")] - public McpExecuteSamplingRequest Request { get => field ??= new(); set; } + /// Optional progress message appended to recent activity when nonempty. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Caller-provided unique identifier for this sampling execution. Use this same ID with cancelSamplingExecution to cancel the in-flight call. Must be unique within the session for the lifetime of the call. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Optional completion percentage; null clears the current percentage. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } - /// Name of the MCP server that initiated the sampling request. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Optional progress phase; null clears the current phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Optional active status transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public TaskClientActiveStatus? Status { get; set; } } -/// Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. +/// Reports successful terminal completion. +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpCancelSamplingExecutionResult +public partial class TaskClientUpdateCompleted : TaskClientUpdate { - /// True if an in-flight execution with the given requestId was found and signalled to cancel. False when no such execution is in flight (already completed, never started, or cancelled by another caller). - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// + [JsonIgnore] + public override string Kind => "completed"; + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional opaque successful terminal result. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } } -/// The requestId previously passed to executeSampling that should be cancelled. +/// Reports terminal failure. +/// The failed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpCancelSamplingExecutionParams +public partial class TaskClientUpdateFailed : TaskClientUpdate { - /// The requestId previously passed to executeSampling that should be cancelled. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "failed"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public required string Error { get; set; } + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } } -/// Env-value mode recorded on the session after the update. +/// Reports terminal cancellation after external work stopped. +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpSetEnvValueModeResult +public partial class TaskClientUpdateCancelled : TaskClientUpdate { - /// Mode recorded on the session after the update. - [JsonPropertyName("mode")] - public McpSetEnvValueModeDetails Mode { get; set; } + /// + [JsonIgnore] + public override string Kind => "cancelled"; + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional human-readable cancellation reason. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reason")] + public string? Reason { get; set; } } -/// Mode controlling how MCP server env values are resolved (`direct` or `indirect`). +/// Updates a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpSetEnvValueModeParams +internal sealed class TasksUpdateRequest { - /// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". - [JsonPropertyName("mode")] - public McpSetEnvValueModeDetails Mode { get; set; } + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Owner update sequence to apply. + [JsonPropertyName("sequence")] + public long Sequence { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Progress or terminal update payload. + [JsonPropertyName("update")] + public TaskClientUpdate Update { get => field ??= new(); set; } } -/// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). +/// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpRemoveGitHubResult +public sealed class TasksRefreshResult { - /// True when the auto-managed `github` MCP server was removed; false when no removal happened (e.g. user has explicitly configured a `github` server, or the server was not registered). - [JsonPropertyName("removed")] - public bool Removed { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpRemoveGitHubRequest +internal sealed class SessionTasksRefreshRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of configuring GitHub MCP. +/// Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigureGitHubResult +public sealed class TasksWaitForPendingResult { - /// Whether GitHub MCP configuration changed. - [JsonPropertyName("changed")] - public bool Changed { get; set; } } -/// Credential-free authentication identity used to configure GitHub MCP. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigureGitHubRequest +internal sealed class SessionTasksWaitForPendingRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. +/// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpStartServerRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(TaskProgressAgent), "agent")] +[JsonDerivedType(typeof(TaskProgressClient), "client")] +[JsonDerivedType(typeof(TaskProgressShell), "shell")] +public partial class TaskProgress { - /// MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). - [JsonPropertyName("config")] - public JsonElement? Config { get; set; } - - /// Exact receipt identity for explicit owned activation in this session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } - - /// Name of the MCP server to start. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. + +/// Timestamped display line for task progress output or recent agent activity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStartServerRequestWithSession +public sealed class TaskProgressLine { - /// MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). - [JsonPropertyName("config")] - public JsonElement? Config { get; set; } - - /// Exact receipt identity for explicit owned activation in this session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } - - /// Name of the MCP server to start. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Display message, e.g., "▸ bash", "✓ edit src/foo.ts". + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// ISO 8601 timestamp when this event occurred. + [JsonPropertyName("timestamp")] + public DateTimeOffset Timestamp { get; set; } } -/// Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. +/// Progress snapshot for an agent task, with recent activity lines and optional latest intent. +/// The agent variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpRestartServerRequest +public partial class TaskProgressAgent : TaskProgress { - /// Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). - [JsonPropertyName("config")] - public JsonElement? Config { get; set; } + /// + [JsonIgnore] + public override string Type => "agent"; - /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// The most recent intent reported by the agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("latestIntent")] + public string? LatestIntent { get; set; } - /// Name of the MCP server to restart. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Recent tool execution events converted to display lines. + [JsonPropertyName("recentActivity")] + public required IList RecentActivity { get; set; } } -/// Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. +/// Generic progress for a client-owned task. +/// The client variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpRestartServerRequestWithSession +public partial class TaskProgressClient : TaskProgress { - /// Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). - [JsonPropertyName("config")] - public JsonElement? Config { get; set; } + /// + [JsonIgnore] + public override string Type => "client"; - /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Most recent nonempty progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lastMessage")] + public string? LastMessage { get; set; } - /// Name of the MCP server to restart. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Current completion percentage from zero through one hundred. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Current owner-defined progress phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } + + /// Recent server-timestamped progress messages. + [JsonPropertyName("recentActivity")] + public required IList RecentActivity { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// Current client task lifecycle status. + [JsonPropertyName("status")] + public required TaskClientStatus Status { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public required DateTimeOffset UpdatedAt { get; set; } } -/// Server name for an individual MCP server stop. +/// Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. +/// The shell variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpStopServerRequest +public partial class TaskProgressShell : TaskProgress { - /// Exact owned receipt identity. Stop also forgets this session's durable activation. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// + [JsonIgnore] + public override string Type => "shell"; - /// Name of the MCP server to stop. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Process ID when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pid")] + public long? Pid { get; set; } + + /// Recent stdout/stderr lines from the running shell command. + [JsonPropertyName("recentOutput")] + public required string RecentOutput { get; set; } } -/// Server name for an individual MCP server stop. +/// Progress information for the task, or null when no task with that ID is tracked. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStopServerRequestWithSession +public sealed class TasksGetProgressResult { - /// Exact owned receipt identity. Stop also forgets this session's durable activation. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } - - /// Name of the MCP server to stop. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. + [JsonPropertyName("progress")] + public TaskProgress? Progress { get; set; } } -/// Registration parameters for an external MCP client. +/// Identifier of the background task to fetch progress for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpRegisterExternalClientRequest +internal sealed class TasksGetProgressRequest { - /// Logical server name for the external client. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Task identifier (agent ID or shell ID). + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Server name identifying the external client to remove. +/// The first sync-waiting task that can currently be promoted to background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpUnregisterExternalClientRequest +public sealed class TasksGetCurrentPromotableResult { - /// Server name of the external client to unregister. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// The first sync-waiting task (agent first, then shell) that can currently be promoted to background mode. Omitted if no such task exists. The returned task is guaranteed to have executionMode='sync' and canPromoteToBackground=true at the time of the call. + [JsonPropertyName("task")] + public TaskInfo? Task { get; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionTasksGetCurrentPromotableRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Whether the named MCP server is running. +/// Indicates whether the task was successfully promoted to background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpIsServerRunningResult +public sealed class TasksPromoteToBackgroundResult { - /// True if the server has an active client and transport. - [JsonPropertyName("running")] - public bool Running { get; set; } + /// Whether the task was successfully promoted to background mode. + [JsonPropertyName("promoted")] + public bool Promoted { get; set; } } -/// Server name to check running status for. +/// Identifier of the task to promote to background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpIsServerRunningRequest +internal sealed class TasksPromoteToBackgroundRequest { - /// Name of the MCP server to check. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the pending MCP OAuth response was accepted. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthHandlePendingResult -{ - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } -} - -/// Host response to the pending OAuth request. -/// Polymorphic base type discriminated by kind. +/// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(McpOauthPendingRequestResponseToken), "token")] -[JsonDerivedType(typeof(McpOauthPendingRequestResponseCancelled), "cancelled")] -public partial class McpOauthPendingRequestResponse +public sealed class TasksPromoteCurrentToBackgroundResult { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. Atomic operation: avoids the race window of getCurrentPromotable + promoteToBackground. + [JsonPropertyName("task")] + public TaskInfo? Task { get; set; } } - -/// The token variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthPendingRequestResponseToken : McpOauthPendingRequestResponse +internal sealed class SessionTasksPromoteCurrentToBackgroundRequest { - /// - [JsonIgnore] - public override string Kind => "token"; - - /// Access token acquired by the SDK host. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } - - /// Token lifetime in seconds, if known. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiresIn")] - public long? ExpiresIn { get; set; } - - /// OAuth token type. Defaults to bearer when omitted. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("tokenType")] - public string? TokenType { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The cancelled variant of . +/// Indicates whether the background task was successfully cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthPendingRequestResponseCancelled : McpOauthPendingRequestResponse +public sealed class TasksCancelResult { - /// - [JsonIgnore] - public override string Kind => "cancelled"; + /// Whether the task was successfully cancelled. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// Pending MCP OAuth request ID and host-provided token or cancellation response. +/// Identifier of the background task to cancel. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthHandlePendingRequest +internal sealed class TasksCancelRequest { - /// OAuth request identifier from the mcp.oauth_required event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; - - /// Host response to the pending OAuth request. - [JsonPropertyName("result")] - public McpOauthPendingRequestResponse Result { get => field ??= new(); set; } + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the MCP server whose persisted OAuth credentials were updated. +/// Indicates whether the task was removed. False when the task does not exist or is still running/idle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthAuthenticationStateChangedRequest +public sealed class TasksRemoveResult { - /// Whether the target session must mint a session-scoped access token instead of reusing a shared access token persisted by another session. - [JsonPropertyName("refreshSessionToken")] - public bool? RefreshSessionToken { get; set; } + /// Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). + [JsonPropertyName("removed")] + public bool Removed { get; set; } +} - /// Name of the MCP server whose OAuth credentials were updated. Omit only when the host cannot identify the server. - [JsonPropertyName("serverName")] - public string? ServerName { get; set; } +/// Identifier of the completed or cancelled task to remove from tracking. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksRemoveRequest +{ + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. +/// Indicates whether the message was delivered, with an error message when delivery failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionMcpOauthPrepareLoginResult +public sealed class TasksSendMessageResult { - /// Original expiry, not extended by consumption, retries or cancellation. - [JsonPropertyName("expiresAt")] - public DateTimeOffset ExpiresAt { get; set; } + /// Error message if delivery failed. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Retain with the original requester and use for one login or cancellation. - [JsonPropertyName("loginId")] - public string LoginId { get; set; } = string.Empty; + /// Whether the message was successfully delivered or steered. + [JsonPropertyName("sent")] + public bool Sent { get; set; } } -/// Effect-free preparation bound to the existing local session, requester and installation, with frozen options. +/// Identifier of the target agent task, message content, and optional sender agent ID. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpOauthPrepareLoginRequest +internal sealed class TasksSendMessageRequest { - /// Text shown on the loopback callback page after successful authorisation. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MaxLength(2048)] - [JsonPropertyName("callbackSuccessMessage")] - public string? CallbackSuccessMessage { get; set; } - - /// Display name used by the incumbent OAuth client-registration flow. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MaxLength(2048)] - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } - - /// Exact installation identity from owned inventory, never a server-name alias. - [JsonPropertyName("expectedInstallationId")] - public string ExpectedInstallationId { get; set; } = string.Empty; + /// Agent ID of the sender, if sent on behalf of another agent. + [JsonPropertyName("fromAgentId")] + public string? FromAgentId { get; set; } - /// Request a new authorisation rather than accepting a usable cached grant. - [JsonPropertyName("forceReauth")] - public bool? ForceReauth { get; set; } + /// Agent task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Name recorded by the authoritative owned installation receipt. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Message content to send to the agent. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthLoginResult -{ - /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. When present, the runtime starts the callback listener before returning and continues the flow in the background; completion is signaled via session.mcp_server_status_changed. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("authorizationUrl")] - public string? AuthorizationUrl { get; set; } - - /// Runtime-issued owned flow identity; never a server name or installation operation ID. - [JsonPropertyName("loginId")] - public string? LoginId { get; set; } - - /// Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. - [JsonPropertyName("status")] - public McpOwnedOauthLoginStatus? Status { get; set; } -} - -/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. +/// Skill metadata available to a session, with name, description, source, enabled/invocable state, path, plugin, and argument hint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthLoginRequest +public sealed class Skill { - /// Optional override for the body text shown on the OAuth loopback callback success page. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass surface-specific copy telling the user where to return. - [JsonPropertyName("callbackSuccessMessage")] - public string? CallbackSuccessMessage { get; set; } - - /// Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. - [JsonPropertyName("clientId")] - public string? ClientId { get; set; } + /// Optional freeform hint describing the skill's expected arguments, from the `argument-hint` frontmatter field. + [JsonPropertyName("argumentHint")] + public string? ArgumentHint { get; set; } - /// Optional override for the OAuth client display name shown on the consent screen. Applies to newly registered dynamic clients only — existing registrations keep the name they were created with. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass their own surface-specific label so the consent screen matches the product the user sees. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// Canonical slash command name used to invoke the skill, without the leading '/'. + [JsonPropertyName("commandName")] + public string? CommandName { get; set; } - /// Optional OAuth client secret override for this login. The runtime treats this as an ephemeral host-owned secret, uses it for this authentication attempt and does not persist it. - [JsonPropertyName("clientSecret")] - public string? ClientSecret { get; set; } + /// Description of what the skill does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Exact owned receipt identity. Owned login never uses an implicit helper session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Whether the skill is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// When true, clears any cached OAuth token for the server and runs a full new authorization. Use when the user explicitly wants to switch accounts or believes their session is stuck. - [JsonPropertyName("forceReauth")] - public bool? ForceReauth { get; set; } + /// Unique identifier for the skill. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. - [JsonPropertyName("grantType")] - public McpOauthLoginGrantType? GrantType { get; set; } + /// Absolute path to the skill file. + [JsonPropertyName("path")] + public string? Path { get; set; } - /// - /// Required for owned login. Consumes the exact prepareLogin handle once. - /// Set forceReauth and display options during preparation, not consumption. - /// - [JsonPropertyName("loginId")] - public string? LoginId { get; set; } + /// Name of the plugin that provides the skill, when source is 'plugin'. + [JsonPropertyName("pluginName")] + public string? PluginName { get; set; } - /// Optional override indicating whether the static OAuth client is public. When false, the runtime treats it as confidential and uses the per-login clientSecret if provided, otherwise retrieving the client secret from the MCP OAuth secret store. - [JsonPropertyName("publicClient")] - public bool? PublicClient { get; set; } + /// Source location type (e.g., project, personal-copilot, plugin, builtin). + [JsonPropertyName("source")] + public SkillSource Source { get; set; } - /// Name of the remote MCP server to authenticate. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Whether the skill can be invoked by the user as a slash command. + [JsonPropertyName("userInvocable")] + public bool UserInvocable { get; set; } } -/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. +/// Skills available to the session, with their enabled state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthLoginRequestWithSession +public sealed class SkillList { - /// Optional override for the body text shown on the OAuth loopback callback success page. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass surface-specific copy telling the user where to return. - [JsonPropertyName("callbackSuccessMessage")] - public string? CallbackSuccessMessage { get; set; } - - /// Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. - [JsonPropertyName("clientId")] - public string? ClientId { get; set; } - - /// Optional override for the OAuth client display name shown on the consent screen. Applies to newly registered dynamic clients only — existing registrations keep the name they were created with. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass their own surface-specific label so the consent screen matches the product the user sees. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } - - /// Optional OAuth client secret override for this login. The runtime treats this as an ephemeral host-owned secret, uses it for this authentication attempt and does not persist it. - [JsonPropertyName("clientSecret")] - public string? ClientSecret { get; set; } - - /// Exact owned receipt identity. Owned login never uses an implicit helper session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } - - /// When true, clears any cached OAuth token for the server and runs a full new authorization. Use when the user explicitly wants to switch accounts or believes their session is stuck. - [JsonPropertyName("forceReauth")] - public bool? ForceReauth { get; set; } - - /// Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. - [JsonPropertyName("grantType")] - public McpOauthLoginGrantType? GrantType { get; set; } - - /// - /// Required for owned login. Consumes the exact prepareLogin handle once. - /// Set forceReauth and display options during preparation, not consumption. - /// - [JsonPropertyName("loginId")] - public string? LoginId { get; set; } - - /// Optional override indicating whether the static OAuth client is public. When false, the runtime treats it as confidential and uses the per-login clientSecret if provided, otherwise retrieving the client secret from the MCP OAuth secret store. - [JsonPropertyName("publicClient")] - public bool? PublicClient { get; set; } - - /// Name of the remote MCP server to authenticate. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Available skills. + [JsonPropertyName("skills")] + public IList Skills { get => field ??= []; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionSkillsListRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Passive MCP OAuth probe result. `authenticated` means the server accepted the probe request while an OAuth-origin access token was attached; it does not prove the server required or independently validated that token. The probe does not make a second unauthenticated request. Failed is an expected probe-domain outcome; JSON-RPC errors are reserved for API-call failures. -/// Polymorphic base type discriminated by status. +/// Skill invocation record with name, path, content, allowed tools, and turn number. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "status", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(McpOauthProbeResultNoAuthRequired), "no-auth-required")] -[JsonDerivedType(typeof(McpOauthProbeResultAuthenticated), "authenticated")] -[JsonDerivedType(typeof(McpOauthProbeResultNeedsAuth), "needs-auth")] -[JsonDerivedType(typeof(McpOauthProbeResultFailed), "failed")] -public partial class McpOauthProbeResult +public sealed class SkillsInvokedSkill { - /// The type discriminator. - [JsonPropertyName("status")] - public virtual string Status { get; set; } = string.Empty; -} + /// Tools that should be auto-approved when this skill is active, captured at invocation time. + [JsonPropertyName("allowedTools")] + public IList? AllowedTools { get; set; } + /// Full content of the skill file. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; -/// The no-auth-required variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultNoAuthRequired : McpOauthProbeResult -{ - /// - [JsonIgnore] - public override string Status => "no-auth-required"; + /// Whether model invocation was disabled when this skill was invoked. + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } - /// HTTP response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } -} + /// Turn number when the skill was invoked. + [JsonPropertyName("invokedAtTurn")] + public long InvokedAtTurn { get; set; } -/// The authenticated variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultAuthenticated : McpOauthProbeResult -{ - /// - [JsonIgnore] - public override string Status => "authenticated"; + /// Unique identifier for the skill. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// HTTP response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } + /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; } -/// The needs-auth variant of . +/// Skills invoked during this session, ordered by invocation time (most recent last). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultNeedsAuth : McpOauthProbeResult +public sealed class SkillsGetInvokedResult { - /// - [JsonIgnore] - public override string Status => "needs-auth"; - - /// HTTP 401 or 403 response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } - - /// Why authentication is needed. - [JsonPropertyName("reason")] - public required McpOauthProbeNeedsAuthReason Reason { get; set; } - - /// Parsed WWW-Authenticate challenge parameters, when present and parseable. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("wwwAuthenticateParams")] - public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; } + /// Skills invoked during this session, ordered by invocation time (most recent last). + [JsonPropertyName("skills")] + public IList Skills { get => field ??= []; set; } } -/// The failed variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultFailed : McpOauthProbeResult +internal sealed class SessionSkillsGetInvokedRequest { - /// - [JsonIgnore] - public override string Status => "failed"; - - /// Human-readable probe failure detail. - [JsonPropertyName("error")] - public required string Error { get; set; } - - /// HTTP response returned by the server, when the probe reached the server and captured the complete response. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("httpResponse")] - public McpOauthHttpResponse? HttpResponse { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Remote MCP server name for a passive OAuth status probe. +/// Name of the skill to enable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthProbeRequest +internal sealed class SkillsEnableRequest { - /// Exact owned receipt identity; probing never activates a dormant installation. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Name of the skill to enable. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Name of the configured remote MCP server to probe. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Remote MCP server name for a passive OAuth status probe. +/// Name of the skill to disable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthProbeRequestWithSession +internal sealed class SkillsDisableRequest { - /// Exact owned receipt identity; probing never activates a dormant installation. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } - - /// Name of the configured remote MCP server to probe. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Name of the skill to disable. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. +/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionMcpOauthCancelLoginResult +public sealed class SkillsLoadDiagnostics { - /// True after cancellation settles, false when the original login already connected successfully. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// Errors emitted while loading skills (e.g. skills that failed to load entirely). + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } + + /// Warnings emitted while loading skills (e.g. skills that loaded but had issues). + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// Targets only the original prepared/applying owned login on this exact session requester. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpOauthCancelLoginRequest +internal sealed class SessionSkillsReloadRequest { - /// The same authoritative installation identity supplied during preparation. - [JsonPropertyName("expectedInstallationId")] - public string ExpectedInstallationId { get; set; } = string.Empty; - - /// Runtime-issued login handle known before the effectful login request begins. - [JsonPropertyName("loginId")] - public string LoginId { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionSkillsEnsureLoadedRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the pending MCP OAuth response was accepted. +/// The IDE a host is connected to, as reported to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthRespondResult +public sealed class SessionConnectedIdeInfo { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Display name of the connected IDE, for example `VS Code`. + [JsonPropertyName("ideName")] + public string IdeName { get; set; } = string.Empty; + + /// Absolute path of the workspace folder the IDE has open. + [JsonPropertyName("workspaceFolder")] + public string WorkspaceFolder { get; set; } = string.Empty; } -/// Pending MCP OAuth request id to respond to. +/// Records which IDE the host is connected to, or clears it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthRespondRequest +internal sealed class SessionMcpSetConnectedIdeInfoParams { - /// OAuth request identifier from the mcp.oauth_required event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + [JsonPropertyName("ide")] + public SessionConnectedIdeInfo? Ide { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the pending MCP headers refresh response was accepted. +/// Recorded MCP server connection failure. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult +public sealed class McpServerFailureInfo { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Failure message produced when the MCP server connection failed. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// epoch-ms timestamp at which the failure was recorded. + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } } -/// Host response: supply dynamic headers or decline this refresh. -/// Polymorphic base type discriminated by kind. +/// Recorded MCP server pending-auth state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestHeaders), "headers")] -[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestNone), "none")] -[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestError), "error")] -public partial class McpHeadersHandlePendingHeadersRefreshRequest +public sealed class McpServerNeedsAuthInfo { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// epoch-ms timestamp at which the server signalled it needs authentication. + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } } - -/// The headers variant of . +/// Host-level state, omitted when no MCP host is initialized. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHeadersHandlePendingHeadersRefreshRequest +public sealed class McpHostState { - /// - [JsonIgnore] - public override string Kind => "headers"; + /// Names of currently-connected MCP clients. + [JsonPropertyName("clients")] + public IList Clients { get => field ??= []; set; } - /// Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. - [JsonPropertyName("headers")] - public required IDictionary Headers { get; set; } + /// Configured servers that are explicitly disabled. + [JsonPropertyName("disabledServers")] + public IList DisabledServers { get => field ??= []; set; } - /// Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("ttlMs")] - public long? TtlMs { get; set; } + /// Map of server name to recorded connection failure. + [JsonPropertyName("failedServers")] + public IDictionary FailedServers { get => field ??= new Dictionary(); set; } + + /// Configured servers filtered out by MCP server policy. + [JsonPropertyName("filteredServers")] + public IList FilteredServers { get => field ??= []; set; } + + /// Whether third-party MCP servers are policy-enabled for this session. + [JsonPropertyName("mcp3pEnabled")] + public bool Mcp3pEnabled { get; set; } + + /// Map of server name to recorded pending-auth state. + [JsonPropertyName("needsAuthServers")] + public IDictionary NeedsAuthServers { get => field ??= new Dictionary(); set; } + + /// Names of servers with in-flight connection attempts. + [JsonPropertyName("pendingConnections")] + public IList PendingConnections { get => field ??= []; set; } } -/// The none variant of . +/// Owned installation that a listed MCP server's live configuration came from. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeadersHandlePendingHeadersRefreshRequest +public sealed class McpServerOwnership { - /// - [JsonIgnore] - public override string Kind => "none"; + /// Stable installation identifier from the owned installation receipt. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; } -/// The error variant of . +/// MCP server status entry, including config source/plugin source and any connection error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestError : McpHeadersHandlePendingHeadersRefreshRequest +public sealed class McpServer { - /// - [JsonIgnore] - public override string Kind => "error"; + /// Human-readable display name supplied by a managed server catalog. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Host credential broker failure, denial, or revocation reason. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Error message if the server failed to connect. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Server name (config key). + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Owned installation this entry's live configuration came from. Absent for manual, workspace, plugin, builtin and same-name servers, and on runtimes without owned installations. + [JsonPropertyName("owned")] + public McpServerOwnership? Owned { get; set; } + + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + [JsonPropertyName("serverMetadata")] + public McpServerMetadata? ServerMetadata { get; set; } + + /// Configuration source: user, workspace, plugin, builtin, or managed. + [JsonPropertyName("source")] + public McpServerSource? Source { get; set; } + + /// Plugin name that provided this server, when source is plugin. + [JsonPropertyName("sourcePlugin")] + public string? SourcePlugin { get; set; } + + /// Plugin version that provided this server, when source is plugin. + [JsonPropertyName("sourcePluginVersion")] + public string? SourcePluginVersion { get; set; } + + /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured. + [JsonPropertyName("status")] + public McpServerStatus Status { get; set; } + + /// Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// MCP headers refresh request id and the host response. +/// MCP servers configured for the session, with their connection status and host-level state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest +public sealed class McpServerList { - /// Headers refresh request identifier from mcp.headers_refresh_required. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Host-level state, omitted when no MCP host is initialized. + [JsonPropertyName("host")] + public McpHostState? Host { get; set; } - /// Host response: supply dynamic headers or decline this refresh. - [JsonPropertyName("result")] - public McpHeadersHandlePendingHeadersRefreshRequest Result { get => field ??= new(); set; } + /// Configured MCP servers. + [JsonPropertyName("servers")] + public IList Servers { get => field ??= []; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpListRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// MCP Apps resource content with URI, optional MIME type, text or base64 blob, and resource metadata. +/// Observational state for a matching already materialized MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsResourceContent +public sealed class McpConfiguredServerState { - /// Resource-level metadata (CSP, permissions, etc.). - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } + /// Observed connection error, when the materialized server failed. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Base64-encoded binary content. - [JsonPropertyName("blob")] - public string? Blob { get; set; } + /// Observed connection status. This is not a configuration or readiness guarantee. + [JsonPropertyName("status")] + public McpServerStatus Status { get; set; } +} - /// MIME type of the content. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpConfiguredServer +{ + /// Human-readable display name supplied by configuration. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Text content (e.g. HTML). - [JsonPropertyName("text")] - public string? Text { get; set; } + /// Whether this configured server is enabled after session configuration and policy filtering. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// The resource URI (typically ui://...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + [JsonPropertyName("live")] + public McpConfiguredServerState? Live { get; set; } + + /// Server name (config key). + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Configuration provenance: user, workspace, plugin, builtin, or managed. + [JsonPropertyName("source")] + public McpServerSource? Source { get; set; } + + /// Plugin name that provided this server, when source is plugin. + [JsonPropertyName("sourcePlugin")] + public string? SourcePlugin { get; set; } + + /// Plugin version that provided this server, when source is plugin. + [JsonPropertyName("sourcePluginVersion")] + public string? SourcePluginVersion { get; set; } } -/// Resource contents returned by the MCP server. +/// Effective MCP configuration with optional live observations from matching already materialized servers. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsReadResourceResult +public sealed class McpConfiguredServerList { - /// Resource contents returned by the server. - [JsonPropertyName("contents")] - public IList Contents { get => field ??= []; set; } + /// Effective configured MCP servers. + [JsonPropertyName("servers")] + public IList Servers { get => field ??= []; set; } } -/// MCP server and resource URI to fetch. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsReadResourceRequest +internal sealed class SessionMcpListConfiguredRequest { - /// Name of the MCP server hosting the resource. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// Resource URI (typically ui://...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; +/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpToolUi +{ + /// URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata. + [JsonPropertyName("resourceUri")] + public string? ResourceUri { get; set; } + + /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + [JsonPropertyName("visibility")] + public IList? Visibility { get; set; } } -/// App-callable tools from the named MCP server. +/// MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsListToolsResult +public sealed class McpTools { - /// App-callable tools from the server. - [JsonPropertyName("tools")] - public IList> Tools { get => field ??= []; set; } + /// Tool description, when provided. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Tool name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` block was present without recognized fields. + [JsonPropertyName("ui")] + public McpToolUi? Ui { get; set; } } -/// MCP server to list app-callable tools for. +/// Tools exposed by the connected MCP server. Throws when the server is not connected. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsListToolsRequest +public sealed class McpListToolsResult { - /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("originServerName")] - public string OriginServerName { get; set; } = string.Empty; + /// Tools exposed by the server. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } +} - /// MCP server hosting the app. +/// Server name whose tool list should be returned. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpListToolsRequest +{ + /// Name of the connected MCP server whose tools to list. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] [MinLength(1)] @@ -15715,22 +16546,31 @@ internal sealed class McpAppsListToolsRequest public string SessionId { get; set; } = string.Empty; } -/// MCP server, tool name, and arguments to invoke from an MCP App view. +/// Name of the MCP server to enable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsCallToolRequest +public sealed class McpEnableRequest { - /// Tool arguments. - [JsonPropertyName("arguments")] - public IDictionary? Arguments { get; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + /// Name of the MCP server to enable. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] [MinLength(1)] - [JsonPropertyName("originServerName")] - public string OriginServerName { get; set; } = string.Empty; + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} - /// MCP server hosting the tool. +/// Name of the MCP server to enable for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpEnableRequestWithSession +{ + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } + + /// Name of the MCP server to enable. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] [MinLength(1)] @@ -15740,346 +16580,314 @@ internal sealed class McpAppsCallToolRequest /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// MCP tool name. - [JsonPropertyName("toolName")] - public string ToolName { get; set; } = string.Empty; } -/// Host context advertised to MCP App guests. +/// Name of the MCP server to disable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsSetHostContextDetails +public sealed class McpDisableRequest { - /// Display modes the host supports. - [JsonPropertyName("availableDisplayModes")] - public IList? AvailableDisplayModes { get; set; } - - /// Current display mode (SEP-1865). - [JsonPropertyName("displayMode")] - public McpAppsSetHostContextDetailsDisplayMode? DisplayMode { get; set; } - - /// BCP-47 locale, e.g. 'en-US'. - [JsonPropertyName("locale")] - public string? Locale { get; set; } - - /// Platform type for responsive design. - [JsonPropertyName("platform")] - public McpAppsSetHostContextDetailsPlatform? Platform { get; set; } - - /// UI theme preference per SEP-1865. - [JsonPropertyName("theme")] - public McpAppsSetHostContextDetailsTheme? Theme { get; set; } - - /// IANA timezone, e.g. 'America/New_York'. - [JsonPropertyName("timeZone")] - public string? TimeZone { get; set; } + /// Required for an owned installation; omission preserves only manual-server behaviour. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Host application identifier. - [JsonPropertyName("userAgent")] - public string? UserAgent { get; set; } + /// Name of the MCP server to disable. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Host context to advertise to MCP App guests. +/// Name of the MCP server to disable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsSetHostContextRequest +internal sealed class McpDisableRequestWithSession { - /// Host context advertised to MCP App guests. - [JsonPropertyName("context")] - public McpAppsSetHostContextDetails Context { get => field ??= new(); set; } + /// Required for an owned installation; omission preserves only manual-server behaviour. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } + + /// Name of the MCP server to disable. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Current host context. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsHostContextDetails +internal sealed class SessionMcpReloadRequest { - /// Display modes the host supports. - [JsonPropertyName("availableDisplayModes")] - public IList? AvailableDisplayModes { get; set; } - - /// Current display mode (SEP-1865). - [JsonPropertyName("displayMode")] - public McpAppsHostContextDetailsDisplayMode? DisplayMode { get; set; } - - /// BCP-47 locale, e.g. 'en-US'. - [JsonPropertyName("locale")] - public string? Locale { get; set; } - - /// Platform type for responsive design. - [JsonPropertyName("platform")] - public McpAppsHostContextDetailsPlatform? Platform { get; set; } - - /// UI theme preference per SEP-1865. - [JsonPropertyName("theme")] - public McpAppsHostContextDetailsTheme? Theme { get; set; } - - /// IANA timezone, e.g. 'America/New_York'. - [JsonPropertyName("timeZone")] - public string? TimeZone { get; set; } - - /// Host application identifier. - [JsonPropertyName("userAgent")] - public string? UserAgent { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Current host context advertised to MCP App guests. +/// Result of moving in-flight MCP loading to the background. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsHostContext +public sealed class MoveMcpLoadingToBackgroundResult { - /// Current host context. - [JsonPropertyName("context")] - public McpAppsHostContextDetails Context { get => field ??= new(); set; } + /// Whether an in-flight MCP load was moved to the background, releasing turns that were waiting on it. False when no MCP load was in flight or the waiting turns had already been released. + [JsonPropertyName("movedToBackground")] + public bool MovedToBackground { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpAppsGetHostContextRequest +internal sealed class SessionMcpMoveLoadingToBackgroundRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Capability negotiation snapshot. +/// MCP server allowed by policy, with server name and optional PII-free explanatory note. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseCapability +public sealed class McpAllowedServer { - /// Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers. - [JsonPropertyName("advertised")] - public bool Advertised { get; set; } + /// Allowed server name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on. - [JsonPropertyName("featureFlagEnabled")] - public bool FeatureFlagEnabled { get; set; } + /// PII-free note explaining why the server was allowed. + [JsonPropertyName("redactedNote")] + public string? RedactedNote { get; set; } +} - /// Whether the session has the `mcp-apps` capability. - [JsonPropertyName("sessionHasMcpApps")] - public bool SessionHasMcpApps { get; set; } +/// MCP server whose connection attempt failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpFailedServer +{ + /// The captured connection failure detail. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// The config key of the server that failed to connect. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// What the server returned for this session. +/// MCP server filtered by policy, with name, reason, and optional redacted reason. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseServer +public sealed class McpFilteredServer { - /// Whether the named server is currently connected. - [JsonPropertyName("connected")] - public bool Connected { get; set; } + /// Deprecated. This field is no longer populated. + [EditorBrowsable(EditorBrowsableState.Never)] +#if NET5_0_OR_GREATER + [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] +#endif + [JsonPropertyName("enterpriseName")] + public string? EnterpriseName { get; set; } - /// Up to 5 tool names with `_meta.ui` for quick inspection. - [JsonPropertyName("sampleToolNames")] - public IList SampleToolNames { get => field ??= []; set; } + /// Filtered server name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Total tools returned by the server's tools/list. - [JsonPropertyName("toolCount")] - public double ToolCount { get; set; } + /// Human-readable filter reason. + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; - /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). - [JsonPropertyName("toolsWithUiMeta")] - public double ToolsWithUiMeta { get; set; } + /// PII-free filter reason. + [JsonPropertyName("redactedReason")] + public string? RedactedReason { get; set; } } -/// Diagnostic snapshot of MCP Apps wiring for the named server. +/// MCP server startup filtering result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseResult +internal sealed class McpStartServersResult { - /// Capability negotiation snapshot. - [JsonPropertyName("capability")] - public McpAppsDiagnoseCapability Capability { get => field ??= new(); set; } + /// Non-default servers allowed by policy. + [JsonPropertyName("allowedServers")] + public IList? AllowedServers { get; set; } - /// What the server returned for this session. - [JsonPropertyName("server")] - public McpAppsDiagnoseServer Server { get => field ??= new(); set; } + /// Servers whose connection attempt failed. + [JsonPropertyName("failedServers")] + public IList? FailedServers { get; set; } + + /// Servers filtered out before startup. + [JsonPropertyName("filteredServers")] + public IList FilteredServers { get => field ??= []; set; } } -/// MCP server to diagnose MCP Apps wiring for. +/// Opaque MCP reload configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsDiagnoseRequest +internal sealed class McpReloadWithConfigRequest { - /// MCP server to probe. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// MCP resource content with URI, optional MIME type, text or base64 blob, and resource metadata. +/// MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceContent +public sealed class McpExecuteSamplingResult { - /// Resource-level metadata (CSP, permissions, etc.). - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } - - /// Base64-encoded binary content. - [JsonPropertyName("blob")] - public string? Blob { get; set; } - - /// MIME type of the content. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } - - /// Text content (e.g. HTML). - [JsonPropertyName("text")] - public string? Text { get; set; } - - /// The resource URI. - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; } -/// Resource contents returned by the MCP server. +/// Outcome of an MCP sampling execution: success result, failure error, or cancellation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesReadResult +public sealed class McpSamplingExecutionResult { - /// Resource contents returned by the server. - [JsonPropertyName("contents")] - public IList Contents { get => field ??= []; set; } -} + /// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. + [JsonPropertyName("action")] + public McpSamplingExecutionAction Action { get; set; } -/// MCP server and resource URI to fetch. + /// Error description, present when action='failure'. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. + [JsonPropertyName("result")] + public McpExecuteSamplingResult? Result { get; set; } +} + +/// Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesReadRequest +public sealed class McpExecuteSamplingRequest { - /// Name of the MCP server hosting the resource. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] +} + +/// Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpExecuteSamplingParams +{ + /// The original MCP JSON-RPC request ID (string or number). Used by the runtime to correlate the inference with the originating MCP request for telemetry; this is distinct from `requestId` (which is the schema-level cancellation handle). + [JsonPropertyName("mcpRequestId")] + public JsonElement McpRequestId { get; set; } + + /// Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. + [JsonPropertyName("request")] + public McpExecuteSamplingRequest Request { get => field ??= new(); set; } + + /// Caller-provided unique identifier for this sampling execution. Use this same ID with cancelSamplingExecution to cancel the in-flight call. Must be unique within the session for the lifetime of the call. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Name of the MCP server that initiated the sampling request. [JsonPropertyName("serverName")] public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Resource URI. - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; } -/// Standard MCP resource annotations plus preserved non-standard annotation fields. +/// Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceAnnotations +public sealed class McpCancelSamplingExecutionResult { - /// Server-provided non-standard annotation fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } - - /// Intended audience roles for this resource. - [JsonPropertyName("audience")] - public IList? Audience { get; set; } - - /// Last-modified timestamp hint. - [JsonPropertyName("lastModified")] - public string? LastModified { get; set; } - - /// Priority hint for model/client use. - [JsonPropertyName("priority")] - public double? Priority { get; set; } + /// True if an in-flight execution with the given requestId was found and signalled to cancel. False when no such execution is in flight (already completed, never started, or cancelled by another caller). + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// A resource icon descriptor plus preserved non-standard icon fields. +/// The requestId previously passed to executeSampling that should be cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceIcon +internal sealed class McpCancelSamplingExecutionParams { - /// Server-provided non-standard icon fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } - - /// Icon MIME type, when known. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } - - /// Icon sizes hint. - [JsonPropertyName("sizes")] - public string? Sizes { get; set; } - - /// Icon URI. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; + /// The requestId previously passed to executeSampling that should be cancelled. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Theme hint for this icon. - [JsonPropertyName("theme")] - public string? Theme { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, MIME type, size, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. +/// Env-value mode recorded on the session after the update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResource +public sealed class McpSetEnvValueModeResult { - /// Resource-level metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } - - /// Server-provided non-standard descriptor fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } - - /// Model/client annotations associated with this resource. - [JsonPropertyName("annotations")] - public McpResourceAnnotations? Annotations { get; set; } - - /// Optional description of what this resource represents. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Mode recorded on the session after the update. + [JsonPropertyName("mode")] + public McpSetEnvValueModeDetails Mode { get; set; } +} - /// Icons associated with this resource. - [JsonPropertyName("icons")] - public IList? Icons { get; set; } +/// Mode controlling how MCP server env values are resolved (`direct` or `indirect`). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpSetEnvValueModeParams +{ + /// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". + [JsonPropertyName("mode")] + public McpSetEnvValueModeDetails Mode { get; set; } - /// MIME type of the resource, if known. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// The programmatic name of the resource. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpRemoveGitHubResult +{ + /// True when the auto-managed `github` MCP server was removed; false when no removal happened (e.g. user has explicitly configured a `github` server, or the server was not registered). + [JsonPropertyName("removed")] + public bool Removed { get; set; } +} - /// Resource size in bytes, when known. - [JsonPropertyName("size")] - public long? Size { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpRemoveGitHubRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } +/// Result of configuring GitHub MCP. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpConfigureGitHubResult +{ + /// Whether GitHub MCP configuration changed. + [JsonPropertyName("changed")] + public bool Changed { get; set; } +} - /// The resource URI (e.g. ui://... or file:///...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; +/// Credential-free authentication identity used to configure GitHub MCP. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpConfigureGitHubRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// One page of resources advertised by the named MCP server. +/// Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesListResult +public sealed class McpStartServerRequest { - /// Opaque cursor for the next page, if the server has more resources. - [JsonPropertyName("nextCursor")] - public string? NextCursor { get; set; } + /// MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). + [JsonPropertyName("config")] + public JsonElement? Config { get; set; } - /// Resources advertised by the server (proxied MCP `resources/list`). - [JsonPropertyName("resources")] - public IList Resources { get => field ??= []; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } + + /// Name of the MCP server to start. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// MCP server whose resources to enumerate. +/// Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesListRequest +internal sealed class McpStartServerRequestWithSession { - /// Opaque MCP pagination cursor from a prior `nextCursor` value. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } + /// MCP server configuration (stdio process or remote HTTP/SSE). Omit to start the server with its already-registered configuration (config-free start-by-name). + [JsonPropertyName("config")] + public JsonElement? Config { get; set; } - /// Name of the MCP server whose resources to enumerate. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } + + /// Name of the MCP server to start. [JsonPropertyName("serverName")] public string ServerName { get; set; } = string.Empty; @@ -16088,72 +16896,66 @@ internal sealed class McpResourcesListRequest public string SessionId { get; set; } = string.Empty; } -/// An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, name, and optional title, description, MIME type, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. +/// Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceTemplate +public sealed class McpRestartServerRequest { - /// Resource-template-level metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } - - /// Server-provided non-standard descriptor fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } - - /// Model/client annotations associated with this template. - [JsonPropertyName("annotations")] - public McpResourceAnnotations? Annotations { get; set; } + /// Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). + [JsonPropertyName("config")] + public JsonElement? Config { get; set; } - /// Optional description of what this template is for. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Icons associated with resources matching this template. - [JsonPropertyName("icons")] - public IList? Icons { get; set; } + /// Name of the MCP server to restart. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} - /// MIME type for resources matching this template, if uniform. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } +/// Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpRestartServerRequestWithSession +{ + /// Replacement MCP server configuration (stdio process or remote HTTP/SSE). Omit to restart the server with its already-registered configuration (config-free restart-by-name). + [JsonPropertyName("config")] + public JsonElement? Config { get; set; } - /// The programmatic name of the resource template. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Name of the MCP server to restart. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// An RFC 6570 URI template for constructing resource URIs. - [JsonPropertyName("uriTemplate")] - public string UriTemplate { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// One page of resource templates advertised by the named MCP server. +/// Server name for an individual MCP server stop. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesListTemplatesResult +public sealed class McpStopServerRequest { - /// Opaque cursor for the next page, if the server has more resource templates. - [JsonPropertyName("nextCursor")] - public string? NextCursor { get; set; } + /// Exact owned receipt identity. Stop also forgets this session's durable activation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Resource templates advertised by the server (proxied MCP `resources/templates/list`). - [JsonPropertyName("resourceTemplates")] - public IList ResourceTemplates { get => field ??= []; set; } + /// Name of the MCP server to stop. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// MCP server whose resource templates to enumerate. +/// Server name for an individual MCP server stop. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesListTemplatesRequest +internal sealed class McpStopServerRequestWithSession { - /// Opaque MCP pagination cursor from a prior `nextCursor` value. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } + /// Exact owned receipt identity. Stop also forgets this session's durable activation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Name of the MCP server whose resource templates to enumerate. - [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] + /// Name of the MCP server to stop. [JsonPropertyName("serverName")] public string ServerName { get; set; } = string.Empty; @@ -16162,4557 +16964,4340 @@ internal sealed class McpResourcesListTemplatesRequest public string SessionId { get; set; } = string.Empty; } -/// MCP diagnostic source configuration. +/// Registration parameters for an external MCP client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpDiagnosticSourceConfiguration +internal sealed class McpRegisterExternalClientRequest { - /// Threshold to apply to MCP diagnostic producers in this session. - [JsonPropertyName("level")] - public DiagnosticLogLevel Level { get; set; } + /// Logical server name for the external client. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. +/// Server name identifying the external client to remove. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiagnosticSourcesConfiguration +internal sealed class McpUnregisterExternalClientRequest { - /// MCP diagnostic capture threshold. Omit to leave the current threshold unchanged. - [JsonPropertyName("mcp")] - public McpDiagnosticSourceConfiguration? Mcp { get; set; } + /// Server name of the external client to unregister. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Per-source session diagnostics configuration. +/// Whether the named MCP server is running. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiagnosticsConfiguration +public sealed class McpIsServerRunningResult { - /// Diagnostic thresholds keyed by supported source. - [JsonPropertyName("sources")] - public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } + /// True if the server has an active client and transport. + [JsonPropertyName("running")] + public bool Running { get; set; } } -/// Patch session diagnostic thresholds for explicitly supplied sources. +/// Server name to check running status for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DiagnosticsConfigureRequest +internal sealed class McpIsServerRunningRequest { + /// Name of the MCP server to check. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// Sources to configure. At least one supported source must be supplied. - [JsonPropertyName("sources")] - public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } +/// Indicates whether the pending MCP OAuth response was accepted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpOauthHandlePendingResult +{ + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// One retained session-scoped diagnostic record. Potentially content-bearing diagnostic data is opt-in and must not be exported automatically as telemetry. -/// Polymorphic base type discriminated by source. +/// Host response to the pending OAuth request. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "source", + TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(DiagnosticEntryMcp), "mcp")] -public partial class DiagnosticEntry +[JsonDerivedType(typeof(McpOauthPendingRequestResponseToken), "token")] +[JsonDerivedType(typeof(McpOauthPendingRequestResponseCancelled), "cancelled")] +public partial class McpOauthPendingRequestResponse { /// The type discriminator. - [JsonPropertyName("source")] - public virtual string Source { get; set; } = string.Empty; + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// MCP-specific detail for a source-discriminated diagnostic entry. +/// The token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpDiagnosticDetails +public partial class McpOauthPendingRequestResponseToken : McpOauthPendingRequestResponse { - /// Fresh identifier for the MCP connection attempt, including failed starts. - [JsonPropertyName("connectionId")] - public string ConnectionId { get; set; } = string.Empty; - - /// Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. - [JsonPropertyName("data")] - public string? Data { get; set; } + /// + [JsonIgnore] + public override string Kind => "token"; - /// Protocol-frame direction when kind is protocol. - [JsonPropertyName("direction")] - public McpDiagnosticDirection? Direction { get; set; } + /// Access token acquired by the SDK host. + [JsonPropertyName("accessToken")] + public required string AccessToken { get; set; } - /// Diagnostic record category. - [JsonPropertyName("kind")] - public McpDiagnosticKind Kind { get; set; } + /// Token lifetime in seconds, if known. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("expiresIn")] + public long? ExpiresIn { get; set; } - /// Configured MCP server name. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// OAuth token type. Defaults to bearer when omitted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenType")] + public string? TokenType { get; set; } } -/// The mcp variant of . +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class DiagnosticEntryMcp : DiagnosticEntry +public partial class McpOauthPendingRequestResponseCancelled : McpOauthPendingRequestResponse { /// [JsonIgnore] - public override string Source => "mcp"; - - /// Agent identifier for a subagent host. Omitted for the root agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("agentId")] - public string? AgentId { get; set; } - - /// Typed MCP diagnostic detail. - [JsonPropertyName("details")] - public required McpDiagnosticDetails Details { get; set; } - - /// Severity of this emitted diagnostic record. - [JsonPropertyName("level")] - public required DiagnosticSeverity Level { get; set; } - - /// Human-readable diagnostic summary. - [JsonPropertyName("message")] - public required string Message { get; set; } - - /// Original byte count when a known-size message or data value was truncated. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("originalBytes")] - public long? OriginalBytes { get; set; } - - /// UTC RFC 3339 timestamp captured at the diagnostic source. - [JsonPropertyName("timestamp")] - public required string Timestamp { get; set; } - - /// Whether message or data was truncated to the record-size bound. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("truncated")] - public bool? Truncated { get; set; } + public override string Kind => "cancelled"; } -/// One cursor-addressed page of retained session diagnostics. +/// Pending MCP OAuth request ID and host-provided token or cancellation response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiagnosticsReadResult +internal sealed class McpOauthHandlePendingRequest { - /// Opaque cursor for the next independent read. - [JsonPropertyName("cursor")] - public string Cursor { get; set; } = string.Empty; - - /// Whether the requested cursor remained within the retained buffer window. - [JsonPropertyName("cursorStatus")] - public DiagnosticCursorStatus CursorStatus { get; set; } - - /// Number of records lost before this page when known. Omitted when a buffer generation change makes the count unknowable. - [JsonPropertyName("droppedCount")] - public long? DroppedCount { get; set; } + /// OAuth request identifier from the mcp.oauth_required event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Retained records beginning at the requested cursor. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } + /// Host response to the pending OAuth request. + [JsonPropertyName("result")] + public McpOauthPendingRequestResponse Result { get => field ??= new(); set; } - /// Whether additional retained records follow this page. - [JsonPropertyName("hasMore")] - public bool HasMore { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). +/// Identifies the MCP server whose persisted OAuth credentials were updated. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DiagnosticsReadRequest +internal sealed class McpOauthAuthenticationStateChangedRequest { - /// Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } + /// Whether the target session must mint a session-scoped access token instead of reusing a shared access token persisted by another session. + [JsonPropertyName("refreshSessionToken")] + public bool? RefreshSessionToken { get; set; } - /// Maximum number of records to return, from 1 through 500. Omit for 100. - [JsonPropertyName("max")] - public long? Max { get; set; } + /// Name of the MCP server whose OAuth credentials were updated. Omit only when the host cannot identify the server. + [JsonPropertyName("serverName")] + public string? ServerName { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// Nonempty selection of sources to read. Each source may be listed once. - [JsonPropertyName("sources")] - public IList Sources { get => field ??= []; set; } +/// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionMcpOauthPrepareLoginResult +{ + /// Original expiry, not extended by consumption, retries or cancellation. + [JsonPropertyName("expiresAt")] + public DateTimeOffset ExpiresAt { get; set; } - /// Maximum time in milliseconds to wait for a new record, from 0 through 30000. - [JsonPropertyName("waitMs")] - public int? WaitMs { get; set; } + /// Retain with the original requester and use for one login or cancellation. + [JsonPropertyName("loginId")] + public string LoginId { get; set; } = string.Empty; } -/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +/// Effect-free preparation bound to the existing local session, requester and installation, with frozen options. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCapabilities +internal sealed class SessionMcpOauthPrepareLoginRequest { - /// Connector API contract version. - [JsonPropertyName("apiVersion")] - public long ApiVersion { get; set; } - - /// Current session availability. Disabled availability is reported without making a Connector request. - [JsonPropertyName("availability")] - public ConnectorAvailability Availability { get; set; } - - /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. - [JsonPropertyName("consentContinuation")] - public bool ConsentContinuation { get; set; } + /// Text shown on the loopback callback page after successful authorisation. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MaxLength(2048)] + [JsonPropertyName("callbackSuccessMessage")] + public string? CallbackSuccessMessage { get; set; } - /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. - [JsonPropertyName("maxDeadlineMs")] - public long MaxDeadlineMs { get; set; } + /// Display name used by the incumbent OAuth client-registration flow. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MaxLength(2048)] + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Maximum accepted polling attempts for one continuation call. - [JsonPropertyName("maxPollAttempts")] - public long MaxPollAttempts { get; set; } + /// Exact installation identity from owned inventory, never a server-name alias. + [JsonPropertyName("expectedInstallationId")] + public string ExpectedInstallationId { get; set; } = string.Empty; - /// Maximum accepted delay in milliseconds between polling attempts. - [JsonPropertyName("maxPollIntervalMs")] - public long MaxPollIntervalMs { get; set; } + /// Request a new authorisation rather than accepting a usable cached grant. + [JsonPropertyName("forceReauth")] + public bool? ForceReauth { get; set; } - /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. - [JsonPropertyName("opaqueAccountSelection")] - public bool OpaqueAccountSelection { get; set; } -} + /// Name recorded by the authoritative owned installation receipt. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionConnectorsGetCapabilitiesRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Account-targeted authorization update required by the Connector service. The account ID is an opaque host routing identifier; no credential is included. +/// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorAuthorizationRequirement +public sealed class McpOauthLoginResult { - /// Exact opaque account selection that made the Connector request. - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; + /// Opaque authorization identifier returned only for a host-managed redirect URI. The runtime also sends it as the OAuth state value, so the callback endpoint can read state and pass it with the full callback URL to session.mcp.oauth.complete. + [JsonPropertyName("authorizationId")] + public string? AuthorizationId { get; set; } - /// Stable OAuth scope the selected account must grant. - [JsonPropertyName("scope")] - public ConnectorAuthorizationScope Scope { get; set; } + /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. For the default loopback flow, the runtime starts its listener before returning. With redirectUri, the host receives the callback and completes it through session.mcp.oauth.complete. The runtime continues the flow in the background and signals completion via session.mcp_server_status_changed. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("authorizationUrl")] + public string? AuthorizationUrl { get; set; } + + /// Runtime-issued owned flow identity; never a server name or installation operation ID. + [JsonPropertyName("loginId")] + public string? LoginId { get; set; } + + /// Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. + [JsonPropertyName("status")] + public McpOwnedOauthLoginStatus? Status { get; set; } } -/// Credential-free Connector catalog entry. +/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCatalogEntry +public sealed class McpOauthLoginRequest { - /// Untrusted service description, when present. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Optional override for the body text shown on the OAuth loopback callback success page. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass surface-specific copy telling the user where to return. + [JsonPropertyName("callbackSuccessMessage")] + public string? CallbackSuccessMessage { get; set; } - /// Untrusted display label from the service. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; + /// Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. + [JsonPropertyName("clientId")] + public string? ClientId { get; set; } - /// Canonical Connector name used by lifecycle methods. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Optional override for the OAuth client display name shown on the consent screen. Applies to newly registered dynamic clients only — existing registrations keep the name they were created with. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass their own surface-specific label so the consent screen matches the product the user sees. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Opaque stable runtime IDs currently projected into the session for this Connector. - [JsonPropertyName("runtimeServerIds")] - public IList RuntimeServerIds { get => field ??= []; set; } + /// Optional OAuth client secret override for this login. The runtime treats this as an ephemeral host-owned secret, uses it for this authentication attempt and does not persist it. + [JsonPropertyName("clientSecret")] + public string? ClientSecret { get; set; } - /// Current authoritative service connection state. - [JsonPropertyName("status")] - public ConnectorCatalogStatus Status { get; set; } -} + /// Exact owned receipt identity. Owned login never uses an implicit helper session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } -/// Validated Connector catalog snapshot cached by the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCatalogResult -{ - /// Validated catalog entries in service order. - [JsonPropertyName("connectors")] - public IList Connectors { get => field ??= []; set; } + /// When true, clears any cached OAuth token for the server and runs a full new authorization. Use when the user explicitly wants to switch accounts or believes their session is stuck. + [JsonPropertyName("forceReauth")] + public bool? ForceReauth { get; set; } - /// Unix epoch milliseconds when this snapshot was accepted. - [JsonPropertyName("refreshedAtMs")] - public long RefreshedAtMs { get; set; } + /// Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. + [JsonPropertyName("grantType")] + public McpOauthLoginGrantType? GrantType { get; set; } - /// Monotonically increasing session-local catalog revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } -} + /// + /// Required for owned login. Consumes the exact prepareLogin handle once. + /// Set forceReauth and display options during preparation, not consumption. + /// + [JsonPropertyName("loginId")] + public string? LoginId { get; set; } -/// Live status of one session-owned MCP projection. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorRuntimeStatus -{ - /// Canonical Connector name that owns this server. - [JsonPropertyName("connectorName")] - public string ConnectorName { get; set; } = string.Empty; + /// Optional override indicating whether the static OAuth client is public. When false, the runtime treats it as confidential and uses the per-login clientSecret if provided, otherwise retrieving the client secret from the MCP OAuth secret store. + [JsonPropertyName("publicClient")] + public bool? PublicClient { get; set; } - /// Opaque runtime server ID. - [JsonPropertyName("runtimeServerId")] - public string RuntimeServerId { get; set; } = string.Empty; + /// Optional externally visible HTTPS redirect URI for a host-managed callback endpoint. When supplied, the runtime still owns discovery, PKCE, token exchange, persistence, and reconnect, but does not bind a loopback listener or terminate HTTPS. The URI must not contain query parameters or a fragment and must be registered for the selected CIMD, DCR, or static OAuth client. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("redirectUri")] + public string? RedirectUri { get; set; } - /// Current live MCP host status. - [JsonPropertyName("status")] - public ConnectorMcpStatus Status { get; set; } + /// Name of the remote MCP server to authenticate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorStatus +internal sealed class McpOauthLoginRequestWithSession { - /// Opaque account selection pinned to this session, when one has been selected. - [JsonPropertyName("accountId")] - public string? AccountId { get; set; } + /// Optional override for the body text shown on the OAuth loopback callback success page. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass surface-specific copy telling the user where to return. + [JsonPropertyName("callbackSuccessMessage")] + public string? CallbackSuccessMessage { get; set; } - /// Connector API contract version. - [JsonPropertyName("apiVersion")] - public long ApiVersion { get; set; } + /// Optional OAuth client ID override for this login. When set, the runtime uses this pre-registered static client instead of dynamic client registration. + [JsonPropertyName("clientId")] + public string? ClientId { get; set; } - /// Exact selected account and stable scope requiring an authorization update, when proven by the Connector service. - [JsonPropertyName("authorizationRequirement")] - public ConnectorAuthorizationRequirement? AuthorizationRequirement { get; set; } + /// Optional override for the OAuth client display name shown on the consent screen. Applies to newly registered dynamic clients only — existing registrations keep the name they were created with. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass their own surface-specific label so the consent screen matches the product the user sees. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Current feature and session availability. - [JsonPropertyName("availability")] - public ConnectorAvailability Availability { get; set; } + /// Optional OAuth client secret override for this login. The runtime treats this as an ephemeral host-owned secret, uses it for this authentication attempt and does not persist it. + [JsonPropertyName("clientSecret")] + public string? ClientSecret { get; set; } - /// Latest validated catalog snapshot, when available. - [JsonPropertyName("catalog")] - public ConnectorCatalogResult? Catalog { get; set; } + /// Exact owned receipt identity. Owned login never uses an implicit helper session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Number of active opaque connection continuations. - [JsonPropertyName("pendingConnections")] - public long PendingConnections { get; set; } + /// When true, clears any cached OAuth token for the server and runs a full new authorization. Use when the user explicitly wants to switch accounts or believes their session is stuck. + [JsonPropertyName("forceReauth")] + public bool? ForceReauth { get; set; } - /// Live MCP status for every Connector-owned runtime server. - [JsonPropertyName("runtimeServers")] - public IList RuntimeServers { get => field ??= []; set; } -} + /// Optional OAuth grant type override for this login. Defaults to the server configuration, or authorization_code when no grant type is specified. + [JsonPropertyName("grantType")] + public McpOauthLoginGrantType? GrantType { get; set; } + + /// + /// Required for owned login. Consumes the exact prepareLogin handle once. + /// Set forceReauth and display options during preparation, not consumption. + /// + [JsonPropertyName("loginId")] + public string? LoginId { get; set; } + + /// Optional override indicating whether the static OAuth client is public. When false, the runtime treats it as confidential and uses the per-login clientSecret if provided, otherwise retrieving the client secret from the MCP OAuth secret store. + [JsonPropertyName("publicClient")] + public bool? PublicClient { get; set; } + + /// Optional externally visible HTTPS redirect URI for a host-managed callback endpoint. When supplied, the runtime still owns discovery, PKCE, token exchange, persistence, and reconnect, but does not bind a loopback listener or terminate HTTPS. The URI must not contain query parameters or a fragment and must be registered for the selected CIMD, DCR, or static OAuth client. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("redirectUri")] + public string? RedirectUri { get; set; } + + /// Name of the remote MCP server to authenticate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionConnectorsGetStatusRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. +/// Host-delivered callback for a runtime-managed MCP OAuth login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorAccountRequest +internal sealed class McpOauthCompleteRequest { - /// Opaque account selection ID previously returned by an account discovery API. - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; + /// Opaque identifier returned by session.mcp.oauth.login for the pending external callback. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("authorizationId")] + public string AuthorizationId { get; set; } = string.Empty; + + /// Full externally visible HTTPS callback URL received by the host, including the authorization response query parameters. Applications behind a reverse proxy must reconstruct the public URL rather than passing an internal proxy URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("callbackUrl")] + public string CallbackUrl { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Typed result of initiating or continuing a Connector connection. -/// Polymorphic base type discriminated by kind. +/// Passive MCP OAuth probe result. `authenticated` means the server accepted the probe request while an OAuth-origin access token was attached; it does not prove the server required or independently validated that token. The probe does not make a second unauthenticated request. Failed is an expected probe-domain outcome; JSON-RPC errors are reserved for API-call failures. +/// Polymorphic base type discriminated by status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", + TypeDiscriminatorPropertyName = "status", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ConnectorConnectResultConnected), "connected")] -[JsonDerivedType(typeof(ConnectorConnectResultConsentRequired), "consent_required")] -[JsonDerivedType(typeof(ConnectorConnectResultPending), "pending")] -public partial class ConnectorConnectResult +[JsonDerivedType(typeof(McpOauthProbeResultNoAuthRequired), "no-auth-required")] +[JsonDerivedType(typeof(McpOauthProbeResultAuthenticated), "authenticated")] +[JsonDerivedType(typeof(McpOauthProbeResultNeedsAuth), "needs-auth")] +[JsonDerivedType(typeof(McpOauthProbeResultFailed), "failed")] +public partial class McpOauthProbeResult { /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + [JsonPropertyName("status")] + public virtual string Status { get; set; } = string.Empty; } -/// The service is connected and the session MCP graph was reconciled. -/// The connected variant of . +/// The no-auth-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ConnectorConnectResultConnected : ConnectorConnectResult +public partial class McpOauthProbeResultNoAuthRequired : McpOauthProbeResult { /// [JsonIgnore] - public override string Kind => "connected"; + public override string Status => "no-auth-required"; - /// Fresh authoritative Connector state after MCP reconciliation. - [JsonPropertyName("status")] - public required ConnectorStatus Status { get; set; } + /// HTTP response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } } -/// Host-owned consent is required before bounded continuation can complete. -/// The consent_required variant of . +/// The authenticated variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ConnectorConnectResultConsentRequired : ConnectorConnectResult +public partial class McpOauthProbeResultAuthenticated : McpOauthProbeResult { /// [JsonIgnore] - public override string Kind => "consent_required"; - - /// Validated HTTPS consent URL. The runtime does not open it. - [JsonPropertyName("consentUrl")] - public required string ConsentUrl { get; set; } + public override string Status => "authenticated"; - /// Opaque ID accepted by continueConnection. - [JsonPropertyName("continuationId")] - public required string ContinuationId { get; set; } + /// HTTP response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } } -/// The service is still completing the connection without a consent URL. -/// The pending variant of . +/// The needs-auth variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ConnectorConnectResultPending : ConnectorConnectResult +public partial class McpOauthProbeResultNeedsAuth : McpOauthProbeResult { /// [JsonIgnore] - public override string Kind => "pending"; + public override string Status => "needs-auth"; - /// Opaque ID accepted by continueConnection. - [JsonPropertyName("continuationId")] - public required string ContinuationId { get; set; } + /// HTTP 401 or 403 response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } + + /// Why authentication is needed. + [JsonPropertyName("reason")] + public required McpOauthProbeNeedsAuthReason Reason { get; set; } + + /// Parsed WWW-Authenticate challenge parameters, when present and parseable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("wwwAuthenticateParams")] + public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; } } -/// Selects one Connector and the pinned host-owned account used for its service and MCP authorization. +/// The failed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorConnectRequest +public partial class McpOauthProbeResultFailed : McpOauthProbeResult { - /// Opaque account selection ID. It must match the account already pinned to the session, if any. - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Status => "failed"; - /// Canonical Connector name from the current catalog. - [JsonPropertyName("connectorName")] - public string ConnectorName { get; set; } = string.Empty; + /// Human-readable probe failure detail. + [JsonPropertyName("error")] + public required string Error { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// HTTP response returned by the server, when the probe reached the server and captured the complete response. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("httpResponse")] + public McpOauthHttpResponse? HttpResponse { get; set; } } -/// Explicitly bounded continuation of a pending Connector connection. +/// Remote MCP server name for a passive OAuth status probe. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorContinueRequest +public sealed class McpOauthProbeRequest { - /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. - [JsonPropertyName("continuationId")] - public string ContinuationId { get; set; } = string.Empty; + /// Exact owned receipt identity; probing never activates a dormant installation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. - [JsonPropertyName("deadlineMs")] - public int DeadlineMs { get; set; } + /// Name of the configured remote MCP server to probe. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} - /// Maximum catalog requests made by this call. Must be between one and the capability limit. - [JsonPropertyName("maxAttempts")] - public int MaxAttempts { get; set; } +/// Remote MCP server name for a passive OAuth status probe. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpOauthProbeRequestWithSession +{ + /// Exact owned receipt identity; probing never activates a dormant installation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Delay in milliseconds between attempts. Must not exceed the capability limit. - [JsonPropertyName("pollIntervalMs")] - public int PollIntervalMs { get; set; } + /// Name of the configured remote MCP server to probe. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Authoritative result after disconnect and MCP reconciliation. +/// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorDisconnectResult +public sealed class SessionMcpOauthCancelLoginResult { - /// Whether the service accepted the idempotent disconnect. - [JsonPropertyName("disconnected")] - public bool Disconnected { get; set; } - - /// Fresh authoritative session state after removing Connector-owned MCP servers. - [JsonPropertyName("status")] - public ConnectorStatus Status { get => field ??= new(); set; } + /// True after cancellation settles, false when the original login already connected successfully. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +/// Targets only the original prepared/applying owned login on this exact session requester. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorReconcileRequest +internal sealed class SessionMcpOauthCancelLoginRequest { - /// Opaque account selection ID. It must match the account already pinned to the session, if any. - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; + /// The same authoritative installation identity supplied during preparation. + [JsonPropertyName("expectedInstallationId")] + public string ExpectedInstallationId { get; set; } = string.Empty; - /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. - [JsonPropertyName("refreshCatalog")] - public bool? RefreshCatalog { get; set; } + /// Runtime-issued login handle known before the effectful login request begins. + [JsonPropertyName("loginId")] + public string LoginId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Indicates whether the pending MCP OAuth response was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionConnectorsWithdrawProjectionRequest +public sealed class McpOauthRespondResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Identifies the target session. +/// Pending MCP OAuth request id to respond to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionManagedSettingsGetRequest +internal sealed class McpOauthRespondRequest { + /// OAuth request identifier from the mcp.oauth_required event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Session plugin metadata, with name, marketplace, optional version, and enabled state. +/// Indicates whether the pending MCP headers refresh response was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Plugin +public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult { - /// Opaque stable identity for a direct plugin source. - [JsonPropertyName("directSourceId")] - public string? DirectSourceId { get; set; } - - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } - - /// Whether this managed desired plugin has an installed or live record. - [JsonPropertyName("installed")] - public bool? Installed { get; set; } - - /// Absolute marketplace directory for a live plugin. - [JsonPropertyName("installedFrom")] - public string? InstalledFrom { get; set; } - - /// Whether enterprise managed settings control this plugin. - [JsonPropertyName("managed")] - public bool? Managed { get; set; } + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Enabled state required by enterprise managed settings. - [JsonPropertyName("managedDesiredEnabled")] - public bool? ManagedDesiredEnabled { get; set; } +/// Host response: supply dynamic headers or decline this refresh. +/// Polymorphic base type discriminated by kind. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestHeaders), "headers")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestNone), "none")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestError), "error")] +public partial class McpHeadersHandlePendingHeadersRefreshRequest +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Marketplace the plugin came from. - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// The headers variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHeadersHandlePendingHeadersRefreshRequest +{ + /// + [JsonIgnore] + public override string Kind => "headers"; - /// Runtime plugin provenance, such as "builtin". - [JsonPropertyName("source")] - public string? Source { get; set; } + /// Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. + [JsonPropertyName("headers")] + public required IDictionary Headers { get; set; } - /// Installed version. - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ttlMs")] + public long? TtlMs { get; set; } } -/// Plugins installed for the session, with their enabled state and version metadata. +/// The none variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginList +public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeadersHandlePendingHeadersRefreshRequest { - /// Installed plugins. - [JsonPropertyName("plugins")] - public IList Plugins { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "none"; } -/// Identifies the target session. +/// The error variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsListRequest +public partial class McpHeadersHandlePendingHeadersRefreshRequestError : McpHeadersHandlePendingHeadersRefreshRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "error"; + + /// Host credential broker failure, denial, or revocation reason. + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// Plugin source resolved relative to the session's authoritative working directory. +/// MCP headers refresh request id and the host response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsInstallRequest +internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest { + /// Headers refresh request identifier from mcp.headers_refresh_required. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Host response: supply dynamic headers or decline this refresh. + [JsonPropertyName("result")] + public McpHeadersHandlePendingHeadersRefreshRequest Result { get => field ??= new(); set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Plugin install spec. Accepts the same forms as the CLI: "plugin@marketplace" (marketplace install), "owner/repo" or "owner/repo:subpath" (GitHub direct), an http/https/ssh URL, or a local path. Direct (non-marketplace) installs are deprecated and will produce a deprecationWarning in the result. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; } -/// Name (or spec) of the plugin to uninstall. +/// MCP Apps resource content with URI, optional MIME type, text or base64 blob, and resource metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsUninstallRequestWithSession +public sealed class McpAppsResourceContent { - /// Stable source identity for a direct (non-marketplace) install. Disambiguates uninstall when multiple installed plugins share the same name. - [JsonPropertyName("directSourceId")] - public string? DirectSourceId { get; set; } + /// Resource-level metadata (CSP, permissions, etc.). + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Plugin name or "plugin@marketplace" spec to uninstall. When ambiguous, prefer the fully-qualified spec. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// Text content (e.g. HTML). + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The resource URI (typically ui://...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// Name (or spec) of the plugin to update. +/// Resource contents returned by the MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsUpdateRequestWithSession +public sealed class McpAppsReadResourceResult { - /// Plugin name or "plugin@marketplace" spec to update. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } } -/// Plugin names (or specs) to enable in the session's authoritative working directory. +/// MCP server and resource URI to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsEnableRequest +internal sealed class McpAppsReadResourceRequest { - /// Plugin names or "plugin@marketplace" specs to enable. Unknown names are ignored. Non-marketplace direct installs are always enabled and cannot be toggled via this API. - [JsonPropertyName("names")] - public IList Names { get => field ??= []; set; } + /// Name of the MCP server hosting the resource. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Resource URI (typically ui://...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// Plugin names (or specs) to disable in the session's authoritative working directory. +/// App-callable tools from the named MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsDisableRequest +public sealed class McpAppsListToolsResult { - /// Plugin names or "plugin@marketplace" specs to disable. Unknown names are ignored. Non-marketplace direct installs cannot be disabled via this API; uninstall them instead. Plugin-owned MCP servers are stopped in active sessions immediately; other plugin contributions remain available until each session reloads plugins. - [JsonPropertyName("names")] - public IList Names { get => field ??= []; set; } + /// App-callable tools from the server. + [JsonPropertyName("tools")] + public IList> Tools { get => field ??= []; set; } +} + +/// MCP server to list app-callable tools for. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpAppsListToolsRequest +{ + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("originServerName")] + public string OriginServerName { get; set; } = string.Empty; + + /// MCP server hosting the app. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// RPC data type for SessionPluginsReload operations. +/// MCP server, tool name, and arguments to invoke from an MCP App view. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionPluginsReloadRequest +internal sealed class McpAppsCallToolRequest { - /// When true, skip repo-level hooks during the hook reload. Use before folder trust is confirmed; load them post-trust via `sessions.loadDeferredRepoHooks`. - [JsonPropertyName("deferRepoHooks")] - public bool? DeferRepoHooks { get; set; } + /// Tool arguments. + [JsonPropertyName("arguments")] + public IDictionary? Arguments { get; set; } - /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadCustomAgents")] - public bool? ReloadCustomAgents { get; set; } + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("originServerName")] + public string OriginServerName { get; set; } = string.Empty; - /// Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). - [JsonPropertyName("reloadExtensions")] - public bool? ReloadExtensions { get; set; } + /// MCP server hosting the tool. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). - [JsonPropertyName("reloadHooks")] - public bool? ReloadHooks { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Reload MCP server connections after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadMcp")] - public bool? ReloadMcp { get; set; } + /// MCP tool name. + [JsonPropertyName("toolName")] + public string ToolName { get; set; } = string.Empty; } -/// RPC data type for SessionPluginsReloadRequestWithSession operations. +/// Host context advertised to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsReloadRequestWithSession +public sealed class McpAppsSetHostContextDetails { - /// When true, skip repo-level hooks during the hook reload. Use before folder trust is confirmed; load them post-trust via `sessions.loadDeferredRepoHooks`. - [JsonPropertyName("deferRepoHooks")] - public bool? DeferRepoHooks { get; set; } + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } - /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadCustomAgents")] - public bool? ReloadCustomAgents { get; set; } + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsSetHostContextDetailsDisplayMode? DisplayMode { get; set; } - /// Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). - [JsonPropertyName("reloadExtensions")] - public bool? ReloadExtensions { get; set; } + /// BCP-47 locale, e.g. 'en-US'. + [JsonPropertyName("locale")] + public string? Locale { get; set; } - /// Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). - [JsonPropertyName("reloadHooks")] - public bool? ReloadHooks { get; set; } + /// Platform type for responsive design. + [JsonPropertyName("platform")] + public McpAppsSetHostContextDetailsPlatform? Platform { get; set; } - /// Reload MCP server connections after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadMcp")] - public bool? ReloadMcp { get; set; } + /// UI theme preference per SEP-1865. + [JsonPropertyName("theme")] + public McpAppsSetHostContextDetailsTheme? Theme { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// IANA timezone, e.g. 'America/New_York'. + [JsonPropertyName("timeZone")] + public string? TimeZone { get; set; } + + /// Host application identifier. + [JsonPropertyName("userAgent")] + public string? UserAgent { get; set; } } -/// Identifies the target session. +/// Host context to advertise to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsMarketplacesListRequest +internal sealed class McpAppsSetHostContextRequest { + /// Host context advertised to MCP App guests. + [JsonPropertyName("context")] + public McpAppsSetHostContextDetails Context { get => field ??= new(); set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Marketplace source and optional working directory for relative-path resolution. +/// Current host context. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesAddRequestWithSession +public sealed class McpAppsHostContextDetails { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; - - /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } - /// Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } -} + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsHostContextDetailsDisplayMode? DisplayMode { get; set; } -/// Name of the marketplace to remove and an optional force flag. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesRemoveRequestWithSession -{ - /// When true, also uninstall every plugin sourced from this marketplace. When false (default), removal is a no-op if any plugin from this marketplace is installed and the dependent plugin names are returned in the result. - [JsonPropertyName("force")] - public bool? Force { get; set; } + /// BCP-47 locale, e.g. 'en-US'. + [JsonPropertyName("locale")] + public string? Locale { get; set; } - /// Marketplace name to remove. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Platform type for responsive design. + [JsonPropertyName("platform")] + public McpAppsHostContextDetailsPlatform? Platform { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// UI theme preference per SEP-1865. + [JsonPropertyName("theme")] + public McpAppsHostContextDetailsTheme? Theme { get; set; } -/// Name of the marketplace whose plugin catalog to fetch. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesBrowseRequestWithSession -{ - /// Marketplace name to browse. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// IANA timezone, e.g. 'America/New_York'. + [JsonPropertyName("timeZone")] + public string? TimeZone { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Host application identifier. + [JsonPropertyName("userAgent")] + public string? UserAgent { get; set; } } -/// RPC data type for SessionPluginsMarketplacesRefresh operations. +/// Current host context advertised to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionPluginsMarketplacesRefreshRequest +public sealed class McpAppsHostContext { - /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Current host context. + [JsonPropertyName("context")] + public McpAppsHostContextDetails Context { get => field ??= new(); set; } } -/// RPC data type for SessionPluginsMarketplacesRefreshRequestWithSession operations. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsMarketplacesRefreshRequestWithSession +internal sealed class SessionMcpAppsGetHostContextRequest { - /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. - [JsonPropertyName("name")] - public string? Name { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one. +/// Capability negotiation snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderSessionToken +public sealed class McpAppsDiagnoseCapability { - /// When the token expires, if known. Callers should refresh by calling `getEndpoint` again before this time, or reactively on any 401/403 response from `baseUrl`. - [JsonPropertyName("expiresAt")] - public DateTimeOffset? ExpiresAt { get; set; } - - /// HTTP header name the token must be sent under. - [JsonPropertyName("header")] - public string Header { get; set; } = string.Empty; + /// Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers. + [JsonPropertyName("advertised")] + public bool Advertised { get; set; } - /// The model the token is bound to, when applicable. When set, the token is only valid for requests against this model. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on. + [JsonPropertyName("featureFlagEnabled")] + public bool FeatureFlagEnabled { get; set; } - /// The short-lived token value. - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; + /// Whether the session has the `mcp-apps` capability. + [JsonPropertyName("sessionHasMcpApps")] + public bool SessionHasMcpApps { get; set; } } -/// A snapshot of the provider endpoint the session is currently configured to talk to. +/// What the server returned for this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderEndpoint +public sealed class McpAppsDiagnoseServer { - /// A credential the caller should use with this endpoint. Omitted only when the endpoint accepts unauthenticated requests. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } - - /// Base URL to pass to the LLM client library. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; - - /// HTTP headers the caller must include on every outbound request. - [JsonPropertyName("headers")] - public IDictionary Headers { get => field ??= new Dictionary(); set; } - - /// Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one. - [JsonPropertyName("sessionToken")] - public ProviderSessionToken? SessionToken { get; set; } + /// Whether the named server is currently connected. + [JsonPropertyName("connected")] + public bool Connected { get; set; } - /// Transport to be used for provider requests. - [JsonPropertyName("transport")] - public ProviderEndpointTransport? Transport { get; set; } + /// Up to 5 tool names with `_meta.ui` for quick inspection. + [JsonPropertyName("sampleToolNames")] + public IList SampleToolNames { get => field ??= []; set; } - /// Provider family. Matches the `type` field of a BYOK provider config. - [JsonPropertyName("type")] - public ProviderEndpointType Type { get; set; } + /// Total tools returned by the server's tools/list. + [JsonPropertyName("toolCount")] + public double ToolCount { get; set; } - /// Wire API to be used, when required for the provider type. - [JsonPropertyName("wireApi")] - public ProviderEndpointWireApi? WireApi { get; set; } + /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). + [JsonPropertyName("toolsWithUiMeta")] + public double ToolsWithUiMeta { get; set; } } -/// RPC data type for SessionProviderGetEndpoint operations. +/// Diagnostic snapshot of MCP Apps wiring for the named server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionProviderGetEndpointRequest +public sealed class McpAppsDiagnoseResult { - /// Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// Capability negotiation snapshot. + [JsonPropertyName("capability")] + public McpAppsDiagnoseCapability Capability { get => field ??= new(); set; } + + /// What the server returned for this session. + [JsonPropertyName("server")] + public McpAppsDiagnoseServer Server { get => field ??= new(); set; } } -/// RPC data type for SessionProviderGetEndpointRequestWithSession operations. +/// MCP server to diagnose MCP Apps wiring for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionProviderGetEndpointRequestWithSession +internal sealed class McpAppsDiagnoseRequest { - /// Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// MCP server to probe. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The selectable model entries synthesized for the models added by this call. +/// MCP resource content with URI, optional MIME type, text or base64 blob, and resource metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderAddResult +public sealed class McpResourceContent { - /// Synthesized selectable model entries for the newly added BYOK models, each under its provider-qualified selection id (`provider/id`). Empty when only providers were added. - [JsonPropertyName("models")] - public IList Models { get => field ??= []; set; } + /// Resource-level metadata (CSP, permissions, etc.). + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } + + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } + + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// Text content (e.g. HTML). + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// RPC data type for ProtocolSystemMessageAppendConfig operations. +/// Resource contents returned by the MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageAppendConfig +public sealed class McpResourcesReadResult { - /// Text appended to the standard system prompt. - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// Append-mode discriminator. Omission also selects append mode. - [JsonPropertyName("mode")] - public ProtocolAppendMode? Mode { get; set; } + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } } -/// RPC data type for SystemMessageBlock operations. +/// MCP server and resource URI to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SystemMessageBlock +internal sealed class McpResourcesReadRequest { - /// Whether providers with explicit prompt caching should place a cache breakpoint after this block. - [JsonPropertyName("cacheBreakpoint")] - public bool? CacheBreakpoint { get; set; } + /// Name of the MCP server hosting the resource. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Text content for this system-message block. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Whether the block is static and may be cached independently of dynamic prompt content. - [JsonPropertyName("isStatic")] - public bool? IsStatic { get; set; } + /// Resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// RPC data type for ProtocolSystemMessageReplaceConfig operations. +/// Standard MCP resource annotations plus preserved non-standard annotation fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageReplaceConfig +public sealed class McpResourceAnnotations { - /// Complete replacement system-message text. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Server-provided non-standard annotation fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Optional structured blocks corresponding to the replacement content. - [JsonPropertyName("contentBlocks")] - public IList? ContentBlocks { get; set; } + /// Intended audience roles for this resource. + [JsonPropertyName("audience")] + public IList? Audience { get; set; } - /// Replace-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolReplaceMode Mode { get; set; } + /// Last-modified timestamp hint. + [JsonPropertyName("lastModified")] + public string? LastModified { get; set; } + + /// Priority hint for model/client use. + [JsonPropertyName("priority")] + public double? Priority { get; set; } } -/// RPC data type for ProtocolStaticSectionOverride operations. +/// A resource icon descriptor plus preserved non-standard icon fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolStaticSectionOverride +public sealed class McpResourceIcon { - /// Declarative operation applied to the section. - [JsonPropertyName("action")] - public ProtocolStaticSectionAction Action { get; set; } + /// Server-provided non-standard icon fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Optional content used by replace, append, and prepend operations. - [JsonPropertyName("content")] - public string? Content { get; set; } -} + /// Icon MIME type, when known. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } -/// Polymorphic base type discriminated by action. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "action", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverrideTransform), "transform")] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverridePreserve), "preserve")] -public partial class ProtocolMarkerSectionOverride -{ - /// The type discriminator. - [JsonPropertyName("action")] - public virtual string Action { get; set; } = string.Empty; -} + /// Icon sizes hint. + [JsonPropertyName("sizes")] + public string? Sizes { get; set; } + /// Icon URI. + [JsonPropertyName("src")] + public string Src { get; set; } = string.Empty; -/// The transform variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ProtocolMarkerSectionOverrideTransform : ProtocolMarkerSectionOverride -{ - /// - [JsonIgnore] - public override string Action => "transform"; + /// Theme hint for this icon. + [JsonPropertyName("theme")] + public string? Theme { get; set; } } -/// The preserve variant of . +/// An MCP resource descriptor (spec `Resource`): URI, name, and optional title, description, MIME type, size, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ProtocolMarkerSectionOverridePreserve : ProtocolMarkerSectionOverride +public sealed class McpResource { - /// - [JsonIgnore] - public override string Action => "preserve"; -} - -/// JSON union data type for ProtocolSectionOverride. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSectionOverride -{ - /// Gets the value when this instance contains . - public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } - - /// Gets the value when this instance contains . - public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } + /// Resource-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolStaticSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolStaticSectionOverride = value; - } + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); + /// Model/client annotations associated with this resource. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolMarkerSectionOverride = value; - } + /// Optional description of what this resource represents. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); + /// Icons associated with this resource. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override ProtocolSectionOverride Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - throw new JsonException("Expected JSON object for ProtocolSectionOverride."); - } + /// MIME type of the resource, if known. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - using var document = JsonDocument.ParseValue(ref reader); - var element = document.RootElement; - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "replace" || element.GetProperty("action").GetString() == "remove" || element.GetProperty("action").GetString() == "append" || element.GetProperty("action").GetString() == "prepend"))) - { - var protocolStaticSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolStaticSectionOverride); - return protocolStaticSectionOverride is null ? throw new JsonException("Expected ProtocolStaticSectionOverride value.") : new ProtocolSectionOverride(protocolStaticSectionOverride); - } - if ((element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "transform")) || element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("action", out _) && element.GetProperty("action").ValueKind == JsonValueKind.String && (element.GetProperty("action").GetString() == "preserve")))) - { - var protocolMarkerSectionOverride = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolMarkerSectionOverride); - return protocolMarkerSectionOverride is null ? throw new JsonException("Expected ProtocolMarkerSectionOverride value.") : new ProtocolSectionOverride(protocolMarkerSectionOverride); - } + /// The programmatic name of the resource. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); - } + /// Resource size in bytes, when known. + [JsonPropertyName("size")] + public long? Size { get; set; } - /// - public override void Write(Utf8JsonWriter writer, ProtocolSectionOverride value, JsonSerializerOptions options) - { - if (value.ProtocolStaticSectionOverride is { } protocolStaticSectionOverride) - { - JsonSerializer.Serialize(writer, protocolStaticSectionOverride, RpcJsonContext.Default.ProtocolStaticSectionOverride); - return; - } - if (value.ProtocolMarkerSectionOverride is { } protocolMarkerSectionOverride) - { - JsonSerializer.Serialize(writer, protocolMarkerSectionOverride, RpcJsonContext.Default.ProtocolMarkerSectionOverride); - return; - } + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } - throw new JsonException("No ProtocolSectionOverride variant value is set."); - } - } + /// The resource URI (e.g. ui://... or file:///...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. +/// One page of resources advertised by the named MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageCustomizeConfig +public sealed class McpResourcesListResult { - /// Text appended after the customized sections. - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// Customize-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolCustomizeMode Mode { get; set; } + /// Opaque cursor for the next page, if the server has more resources. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } - /// Named standard-prompt section overrides. - [JsonPropertyName("sections")] - public IDictionary? Sections { get; set; } + /// Resources advertised by the server (proxied MCP `resources/list`). + [JsonPropertyName("resources")] + public IList Resources { get => field ??= []; set; } } -/// JSON union data type for ProtocolSystemMessageConfig. +/// MCP server whose resources to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSystemMessageConfig +internal sealed class McpResourcesListRequest { - /// Gets the value when this instance contains . - public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Gets the value when this instance contains . - public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } + /// Name of the MCP server whose resources to enumerate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Gets the value when this instance contains . - public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageAppendConfig = value; - } +/// An MCP resource template descriptor (spec `ResourceTemplate`): an RFC 6570 URI template, name, and optional title, description, MIME type, icons, annotations, and metadata. Server-provided fields outside the standard descriptor shape are exposed under `additionalProperties`. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpResourceTemplate +{ + /// Resource-template-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageReplaceConfig = value; - } + /// Model/client annotations associated with this template. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) => new(value); + /// Optional description of what this template is for. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageCustomizeConfig = value; - } + /// Icons associated with resources matching this template. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); + /// MIME type for resources matching this template, if uniform. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override ProtocolSystemMessageConfig Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - throw new JsonException("Expected JSON object for ProtocolSystemMessageConfig."); - } + /// The programmatic name of the resource template. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - using var document = JsonDocument.ParseValue(ref reader); - var element = document.RootElement; - if (element.ValueKind == JsonValueKind.Object && (!element.TryGetProperty("mode", out _) || (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "append")))) - { - var protocolSystemMessageAppendConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); - return protocolSystemMessageAppendConfig is null ? throw new JsonException("Expected ProtocolSystemMessageAppendConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageAppendConfig); - } - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "replace"))) - { - var protocolSystemMessageReplaceConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); - return protocolSystemMessageReplaceConfig is null ? throw new JsonException("Expected ProtocolSystemMessageReplaceConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageReplaceConfig); - } - if (element.ValueKind == JsonValueKind.Object && (element.TryGetProperty("mode", out _) && element.GetProperty("mode").ValueKind == JsonValueKind.String && (element.GetProperty("mode").GetString() == "customize"))) - { - var protocolSystemMessageCustomizeConfig = JsonSerializer.Deserialize(element, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); - return protocolSystemMessageCustomizeConfig is null ? throw new JsonException("Expected ProtocolSystemMessageCustomizeConfig value.") : new ProtocolSystemMessageConfig(protocolSystemMessageCustomizeConfig); - } + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } - throw new JsonException("JSON value did not match any ProtocolSystemMessageConfig variant."); - } + /// An RFC 6570 URI template for constructing resource URIs. + [JsonPropertyName("uriTemplate")] + public string UriTemplate { get; set; } = string.Empty; +} - /// - public override void Write(Utf8JsonWriter writer, ProtocolSystemMessageConfig value, JsonSerializerOptions options) - { - if (value.ProtocolSystemMessageAppendConfig is { } protocolSystemMessageAppendConfig) - { - JsonSerializer.Serialize(writer, protocolSystemMessageAppendConfig, RpcJsonContext.Default.ProtocolSystemMessageAppendConfig); - return; - } - if (value.ProtocolSystemMessageReplaceConfig is { } protocolSystemMessageReplaceConfig) - { - JsonSerializer.Serialize(writer, protocolSystemMessageReplaceConfig, RpcJsonContext.Default.ProtocolSystemMessageReplaceConfig); - return; - } - if (value.ProtocolSystemMessageCustomizeConfig is { } protocolSystemMessageCustomizeConfig) - { - JsonSerializer.Serialize(writer, protocolSystemMessageCustomizeConfig, RpcJsonContext.Default.ProtocolSystemMessageCustomizeConfig); - return; - } +/// One page of resource templates advertised by the named MCP server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpResourcesListTemplatesResult +{ + /// Opaque cursor for the next page, if the server has more resource templates. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } - throw new JsonException("No ProtocolSystemMessageConfig variant value is set."); - } - } + /// Resource templates advertised by the server (proxied MCP `resources/templates/list`). + [JsonPropertyName("resourceTemplates")] + public IList ResourceTemplates { get => field ??= []; set; } } -/// A BYOK model definition referencing a named provider. +/// MCP server whose resource templates to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderModelConfig +internal sealed class McpResourcesListTemplatesRequest { - /// Optional capability overrides (vision, tool_calls, reasoning, etc.). - [JsonPropertyName("capabilities")] - public ModelCapabilitiesOverride? Capabilities { get; set; } - - /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Maximum context window tokens for the model. - [JsonPropertyName("maxContextWindowTokens")] - public double? MaxContextWindowTokens { get; set; } + /// Name of the MCP server whose resource templates to enumerate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Maximum output tokens for the model. - [JsonPropertyName("maxOutputTokens")] - public double? MaxOutputTokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Maximum prompt/input tokens for the model. - [JsonPropertyName("maxPromptTokens")] - public double? MaxPromptTokens { get; set; } +/// An argument accepted by an MCP prompt. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpPromptArgument +{ + /// Argument-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } + /// Server-provided non-standard argument fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// Description of the argument. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + /// Name of the argument. [JsonPropertyName("name")] - public string? Name { get; set; } - - /// Name of the configured provider that serves this model. - [JsonPropertyName("provider")] - public string Provider { get; set; } = string.Empty; - - /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. - [JsonPropertyName("systemMessage")] - public ProtocolSystemMessageConfig? SystemMessage { get; set; } + public string Name { get; set; } = string.Empty; - /// The model name sent to the provider API for inference. Defaults to `id`. - [JsonPropertyName("wireModel")] - public string? WireModel { get; set; } + /// Whether the argument is required; omission is distinct from false. + [JsonPropertyName("required")] + public bool? Required { get; set; } } -/// Azure-specific provider options. +/// An MCP prompt icon with standard size hints and preserved non-standard fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderConfigAzure +public sealed class McpPromptIcon { - /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - [JsonPropertyName("apiVersion")] - public string? ApiVersion { get; set; } + /// Server-provided non-standard icon fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Icon MIME type, when known. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// Icon sizes, such as `48x48` or `any`. + [JsonPropertyName("sizes")] + public IList? Sizes { get; set; } + + /// Icon URI. + [JsonPropertyName("src")] + public string Src { get; set; } = string.Empty; + + /// Theme hint for this icon. + [JsonPropertyName("theme")] + public string? Theme { get; set; } } -/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. +/// An MCP prompt descriptor. Server-provided non-standard fields are exposed under `additionalProperties`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NamedProviderConfig +public sealed class McpPrompt { - /// Static API key used to authenticate provider requests. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } - - /// Azure authentication configuration for the provider. - [JsonPropertyName("azure")] - public ProviderConfigAzure? Azure { get; set; } + /// Prompt-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Base URL for provider API requests. - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; + /// Server-provided non-standard descriptor fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Static bearer token used to authenticate provider requests. - [JsonPropertyName("bearerToken")] - public string? BearerToken { get; set; } + /// Arguments accepted by the prompt. + [JsonPropertyName("arguments")] + public IList? Arguments { get; set; } - /// Whether the host supplies bearer tokens dynamically. - [JsonPropertyName("hasBearerTokenProvider")] - public bool? HasBearerTokenProvider { get; set; } + /// Description of what this prompt provides. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Additional HTTP headers included with provider requests. - [JsonPropertyName("headers")] - public IDictionary? Headers { get; set; } + /// Icons associated with this prompt. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } - /// Unique provider name used to qualify model selection IDs. + /// The programmatic name of the prompt. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Transport used to communicate with the provider. - [JsonPropertyName("transport")] - public ProviderConfigTransport? Transport { get; set; } + /// Human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } +} - /// Provider protocol family. - [JsonPropertyName("type")] - public ProviderConfigType? Type { get; set; } +/// One page of prompts advertised by the named MCP server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpPromptsListResult +{ + /// MCP result metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Wire API used to communicate with the provider. - [JsonPropertyName("wireApi")] - public ProviderConfigWireApi? WireApi { get; set; } + /// Server-provided non-standard result fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } + + /// Opaque cursor for the next page, if the server has more prompts. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } + + /// Prompts advertised by the server. + [JsonPropertyName("prompts")] + public IList Prompts { get => field ??= []; set; } } -/// BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. +/// MCP server whose prompts to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ProviderAddRequest +internal sealed class McpPromptsListRequest { - /// BYOK model definitions to register. Each must reference a provider that is already registered or included in this same call. Selection ids (`provider/id`) must be unique across the registry. - [JsonPropertyName("models")] - public IList? Models { get; set; } + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Named BYOK provider connections to register, additive to any providers already in the registry. Each name must be unique across the registry and must not contain '/'. - [JsonPropertyName("providers")] - public IList? Providers { get; set; } + /// Name of the MCP server whose prompts to enumerate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The selectable model entries and selection ids synthesized for the synchronized BYOK models. +/// An MCP prompt message with opaque JSON content preserved without flattening or content-type filtering. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderSyncResult +public sealed class McpPromptMessage { - /// True when synchronization withdrew the selected host-managed model, leaving the session with no explicit selection, so ordinary model resolution picks the session default. Synchronization never promotes a surviving host model in its place: publishing a model offers it, and the choice of which model to use stays with the user. - [JsonPropertyName("modelDeselected")] - public bool? ModelDeselected { get; set; } + /// Message-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Synthesized selectable model entries for the synchronized BYOK models. - [JsonPropertyName("models")] - public IList Models { get => field ??= []; set; } + /// Server-provided non-standard message fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Provider-qualified model selection ids present after synchronization. - [JsonPropertyName("selectionIds")] - public IList SelectionIds { get => field ??= []; set; } + /// The original MCP content block, including nested metadata and unfamiliar content types. + [JsonPropertyName("content")] + public JsonElement Content { get; set; } + + /// The role of the message sender. + [JsonPropertyName("role")] + public McpPromptRole Role { get; set; } } -/// Authoritative BYOK provider and model registry snapshot to apply atomically to the session. +/// Prompt messages returned by the MCP server without sending them to the model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ProviderSyncRequest +public sealed class McpPromptsGetResult { - /// BYOK model definition snapshot. Models absent from this list are removed. - [JsonPropertyName("models")] - public IList? Models { get; set; } + /// MCP result metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Named BYOK provider connection snapshot. Providers absent from this list are removed. - [JsonPropertyName("providers")] - public IList? Providers { get; set; } + /// Server-provided non-standard result fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Description of the prompt. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Ordered prompt messages. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; set; } } -/// What the withdrawal actually removed from the registry. +/// MCP server, prompt name, and optional string-valued arguments. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderWithdrawResult +internal sealed class McpPromptsGetRequest { - /// True when withdrawal removed the selected host-managed model, leaving the session with no explicit selection, so ordinary model resolution picks the session default. Withdrawal never promotes a surviving model in its place: the choice of which model to use stays with the user. - [JsonPropertyName("modelDeselected")] - public bool? ModelDeselected { get; set; } - - /// Providers removed because one of the withdrawn models was the last entry referencing them. A provider that merely has no models is not removed. - [JsonPropertyName("providersRemoved")] - public IList ProvidersRemoved { get => field ??= []; set; } + /// String-valued arguments to pass to the prompt. + [JsonPropertyName("arguments")] + public IDictionary? Arguments { get; set; } - /// Selection ids that were registered and are now withdrawn. Excludes requested ids that were not present. - [JsonPropertyName("withdrawn")] - public IList Withdrawn { get => field ??= []; set; } -} + /// The programmatic name of the prompt. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("promptName")] + public string PromptName { get; set; } = string.Empty; -/// Host-managed model selection ids to withdraw from the session's BYOK registry. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ProviderWithdrawRequest -{ - /// Provider-qualified selection ids to withdraw. Ids that are not registered are ignored, so withdrawal is idempotent. A provider left with no models referencing it is removed too. - [JsonPropertyName("models")] - public IList Models { get => field ??= []; set; } + /// Name of the MCP server hosting the prompt. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the session options patch was applied successfully. +/// MCP diagnostic source configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionUpdateOptionsResult +public sealed class McpDiagnosticSourceConfiguration { - /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated. - [JsonPropertyName("pluginHookCount")] - public long? PluginHookCount { get; set; } - - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Threshold to apply to MCP diagnostic producers in this session. + [JsonPropertyName("level")] + public DiagnosticLogLevel Level { get; set; } } -/// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. +/// Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicyRuleSource +public sealed class DiagnosticSourcesConfiguration { - /// Name of the policy source. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Type of the policy source. - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; + /// MCP diagnostic capture threshold. Omit to leave the current threshold unchanged. + [JsonPropertyName("mcp")] + public McpDiagnosticSourceConfiguration? Mcp { get; set; } } -/// Single content-exclusion rule supplied to `session.options.update`, with paths, match conditions, and source. +/// Per-source session diagnostics configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicyRule +public sealed class DiagnosticsConfiguration { - /// Conditions of which at least one must match. - [JsonPropertyName("ifAnyMatch")] - public IList? IfAnyMatch { get; set; } + /// Diagnostic thresholds keyed by supported source. + [JsonPropertyName("sources")] + public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } +} - /// Conditions none of which may match. - [JsonPropertyName("ifNoneMatch")] - public IList? IfNoneMatch { get; set; } +/// Patch session diagnostic thresholds for explicitly supplied sources. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class DiagnosticsConfigureRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Path patterns covered by this rule. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } + /// Sources to configure. At least one supported source must be supplied. + [JsonPropertyName("sources")] + public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } +} - /// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. +/// One retained session-scoped diagnostic record. Potentially content-bearing diagnostic data is opt-in and must not be exported automatically as telemetry. +/// Polymorphic base type discriminated by source. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "source", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(DiagnosticEntryMcp), "mcp")] +public partial class DiagnosticEntry +{ + /// The type discriminator. [JsonPropertyName("source")] - public OptionsUpdateAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } + public virtual string Source { get; set; } = string.Empty; } -/// Content-exclusion policy supplied to `session.options.update`, with rules, last-updated data, and scope. + +/// MCP-specific detail for a source-discriminated diagnostic entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicy +public sealed class McpDiagnosticDetails { - /// Opaque policy update timestamp supplied by the host. - [JsonPropertyName("last_updated_at")] - public JsonElement LastUpdatedAt { get; set; } + /// Fresh identifier for the MCP connection attempt, including failed starts. + [JsonPropertyName("connectionId")] + public string ConnectionId { get; set; } = string.Empty; - /// Content-exclusion rules to apply. - [JsonPropertyName("rules")] - public IList Rules { get => field ??= []; set; } + /// Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. + [JsonPropertyName("data")] + public string? Data { get; set; } - /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. - [JsonPropertyName("scope")] - public OptionsUpdateAdditionalContentExclusionPolicyScope Scope { get; set; } -} + /// Protocol-frame direction when kind is protocol. + [JsonPropertyName("direction")] + public McpDiagnosticDirection? Direction { get; set; } -/// Options scoped to the built-in CAPI (Copilot API) provider. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CapiSessionOptions -{ - /// Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. `fast` is an integrator-only latency preset, not a first-party GitHub Copilot product preference. - [JsonPropertyName("autoTier")] - public AutoTier? AutoTier { get; set; } + /// Diagnostic record category. + [JsonPropertyName("kind")] + public McpDiagnosticKind Kind { 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; } + /// Configured MCP server name. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; } -/// Installed plugin record for a session, with marketplace, version, install time, enabled state, cache path, and source. +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionInstalledPlugin +public partial class DiagnosticEntryMcp : DiagnosticEntry { - /// Path where the plugin is cached locally. - [JsonPropertyName("cache_path")] - public string? CachePath { get; set; } - - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// + [JsonIgnore] + public override string Source => "mcp"; - /// Installation timestamp (ISO-8601). - [JsonPropertyName("installed_at")] - public string InstalledAt { get; set; } = string.Empty; + /// Agent identifier for a subagent host. Omitted for the root agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } - /// 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; } + /// Typed MCP diagnostic detail. + [JsonPropertyName("details")] + public required McpDiagnosticDetails Details { get; set; } - /// Marketplace the plugin came from (empty string for direct repo installs). - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; + /// Severity of this emitted diagnostic record. + [JsonPropertyName("level")] + public required DiagnosticSeverity Level { get; set; } - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Human-readable diagnostic summary. + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Source descriptor for direct repo installs (when marketplace is empty). - [JsonPropertyName("source")] - public JsonElement? Source { get; set; } + /// Original byte count when a known-size message or data value was truncated. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("originalBytes")] + public long? OriginalBytes { get; set; } - /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. - [JsonPropertyName("source_sha")] - public string? SourceSha { get; set; } + /// UTC RFC 3339 timestamp captured at the diagnostic source. + [JsonPropertyName("timestamp")] + public required string Timestamp { get; set; } - /// Installed version, if known. - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Whether message or data was truncated to the record-size bound. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("truncated")] + public bool? Truncated { get; set; } } -/// Custom model-provider configuration (BYOK). +/// One cursor-addressed page of retained session diagnostics. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderConfig +public sealed class DiagnosticsReadResult { - /// API key. Optional for local providers like Ollama. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } + /// Opaque cursor for the next independent read. + [JsonPropertyName("cursor")] + public string Cursor { get; set; } = string.Empty; - /// Azure-specific provider options. - [JsonPropertyName("azure")] - public ProviderConfigAzure? Azure { get; set; } + /// Whether the requested cursor remained within the retained buffer window. + [JsonPropertyName("cursorStatus")] + public DiagnosticCursorStatus CursorStatus { get; set; } - /// API endpoint URL. - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; + /// Number of records lost before this page when known. Omitted when a buffer generation change makes the count unknowable. + [JsonPropertyName("droppedCount")] + public long? DroppedCount { get; set; } - /// Bearer token for authentication. Sets the Authorization header directly. Takes precedence over apiKey when both are set. - [JsonPropertyName("bearerToken")] - public string? BearerToken { get; set; } + /// Retained records beginning at the requested cursor. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } - /// When true, the SDK client supplies bearer tokens on demand: the runtime calls the client-session `providerToken.getToken` callback before each request and applies the returned token as an `Authorization: Bearer <token>` header. This is the bearer/OAuth scheme used by Azure AD / managed-identity tokens and provider OAuth access tokens (including Anthropic's), not a provider-specific API-key header such as Anthropic's `x-api-key`. The token-acquiring function itself stays on the SDK side and is never serialized; only this flag crosses the wire. When set alongside `apiKey`/`bearerToken`, the callback takes precedence: the runtime applies the token returned by `providerToken.getToken` as the `Authorization: Bearer` header for each request and does not send the static credential. - [JsonPropertyName("hasBearerTokenProvider")] - public bool? HasBearerTokenProvider { get; set; } + /// Whether additional retained records follow this page. + [JsonPropertyName("hasMore")] + public bool HasMore { get; set; } +} - /// Custom HTTP headers to include in all outbound requests to the provider. - [JsonPropertyName("headers")] - public IDictionary? Headers { get; set; } +/// Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class DiagnosticsReadRequest +{ + /// Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Maximum context window tokens for the model. - [JsonPropertyName("maxContextWindowTokens")] - public double? MaxContextWindowTokens { get; set; } + /// Maximum number of records to return, from 1 through 500. Omit for 100. + [JsonPropertyName("max")] + public long? Max { get; set; } - /// Maximum output tokens for the model. - [JsonPropertyName("maxOutputTokens")] - public double? MaxOutputTokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Maximum prompt/input tokens for the model. - [JsonPropertyName("maxPromptTokens")] - public double? MaxPromptTokens { get; set; } + /// Nonempty selection of sources to read. Each source may be listed once. + [JsonPropertyName("sources")] + public IList Sources { get => field ??= []; set; } - /// Overrides for model capabilities when they cannot be inferred from modelId. - [JsonPropertyName("modelCapabilities")] - public ModelCapabilitiesOverride? ModelCapabilities { get; set; } + /// Maximum time in milliseconds to wait for a new record, from 0 through 30000. + [JsonPropertyName("waitMs")] + public int? WaitMs { get; set; } +} - /// Well-known model ID used for capability lookup. When set, agent behavior config and token limits are inferred from this model. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } +/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorCapabilities +{ + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } - /// Provider name used for model and telemetry attribution. - [JsonPropertyName("providerName")] - public string? ProviderName { get; set; } + /// Current session availability. Disabled availability is reported without making a Connector request. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { get; set; } - /// Provider transport. Defaults to "http". - [JsonPropertyName("transport")] - public ProviderConfigTransport? Transport { get; set; } + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + [JsonPropertyName("consentContinuation")] + public bool ConsentContinuation { get; set; } - /// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. - [JsonPropertyName("type")] - public ProviderConfigType? Type { get; set; } + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + [JsonPropertyName("maxDeadlineMs")] + public long MaxDeadlineMs { get; set; } - /// Wire API format (openai/azure only). Defaults to "completions". - [JsonPropertyName("wireApi")] - public ProviderConfigWireApi? WireApi { get; set; } + /// Maximum accepted polling attempts for one continuation call. + [JsonPropertyName("maxPollAttempts")] + public long MaxPollAttempts { get; set; } - /// The model identifier sent to the provider API for inference (the "wire" model), as opposed to modelId which is the well-known base. - [JsonPropertyName("wireModel")] - public string? WireModel { get; set; } -} + /// Maximum accepted delay in milliseconds between polling attempts. + [JsonPropertyName("maxPollIntervalMs")] + public long MaxPollIntervalMs { get; set; } -/// Credential-injection capability flags applied while the sandbox is enabled. For the same capability independent of sandboxing, and matched to the credential's GitHub host, see `shell.credentials`; the two are additive. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigAuth -{ - /// Whether to authenticate sandboxed gh through the local masking proxy. The child receives a fake GH_TOKEN; its real value is substituted only at github.com, api.github.com and uploads.github.com (github.com because gh repo clone authenticates git through gh auth git-credential). The repository's GitHub account takes precedence over the Copilot login. Default: false (opt-in). - [JsonPropertyName("gh")] - public bool? Gh { get; set; } + /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + [JsonPropertyName("opaqueAccountSelection")] + public bool OpaqueAccountSelection { get; set; } - /// Whether to authenticate sandboxed HTTPS git through the local masking proxy. The child receives a fake `http.<url>.extraheader`; the real Authorization header is substituted only at its original HTTPS host, port, and repository path scope. github.com uses the Copilot token; other forges use credentials resolved from the user's own helper on the host. Default: false (opt-in). - [JsonPropertyName("git")] - public bool? Git { get; set; } + /// Whether getAccount is supported. Absence means false. + [JsonPropertyName("sessionAccountSelection")] + public bool? SessionAccountSelection { get; set; } + + /// Whether reconcile accepts forceConnectorName. Absence means false. + [JsonPropertyName("targetedReconcile")] + public bool? TargetedReconcile { get; set; } } -/// Destinations authorized to receive one masked environment credential. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxMaskedEnvVar +internal sealed class SessionConnectorsGetCapabilitiesRequest { - /// Nonempty list of HTTPS injection hostnames or *.example.com patterns. Bare * is not accepted. These grants never override the sandbox network policy. Values in plaintext HTTP requests, URLs, bodies, encoded credentials, and signed requests are not substituted. - [JsonPropertyName("injectHosts")] - public IList InjectHosts { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Whole-value environment credential masking for sandboxed children. +/// Credential-free identity metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxCredentialsConfig +public sealed class AuthIdentityMetadata { - /// Environment variable names and their HTTPS injection destinations. Absent variables stay absent. No real values or sentinels are stored in this map. - [JsonPropertyName("envVars")] - public IDictionary EnvVars { get => field ??= new Dictionary(); set; } + /// Identity host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + /// User login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Authentication type. + [JsonPropertyName("type")] + public AuthInfoType Type { get; set; } } -/// macOS seatbelt experimental options. +/// Session account selection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyExperimentalSeatbelt +public sealed class ConnectorSessionAccount { - /// Whether the macOS seatbelt profile may access the keychain. - [JsonPropertyName("keychainAccess")] - public bool? KeychainAccess { get; set; } + /// Opaque session-scoped account selection ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Credential-free identity metadata. + [JsonPropertyName("authInfo")] + public AuthIdentityMetadata AuthInfo { get => field ??= new(); set; } } -/// Platform-specific experimental policy fields. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyExperimental +internal sealed class SessionConnectorsGetAccountRequest { - /// macOS seatbelt experimental options. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicyExperimentalSeatbelt? Seatbelt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Filesystem rules to merge into the base policy. +/// Account-targeted authorization update required by the Connector service. The account ID is an opaque host routing identifier; no credential is included. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyFilesystem +public sealed class ConnectorAuthorizationRequirement { - /// Whether to clear the policy when the session exits. - [JsonPropertyName("clearPolicyOnExit")] - public bool? ClearPolicyOnExit { get; set; } - - /// Paths explicitly denied. - [JsonPropertyName("deniedPaths")] - public IList? DeniedPaths { get; set; } - - /// Paths granted read-only access. - [JsonPropertyName("readonlyPaths")] - public IList? ReadonlyPaths { get; set; } + /// Exact opaque account selection that made the Connector request. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// Paths granted read/write access. - [JsonPropertyName("readwritePaths")] - public IList? ReadwritePaths { get; set; } + /// Stable OAuth scope the selected account must grant. + [JsonPropertyName("scope")] + public ConnectorAuthorizationScope Scope { get; set; } } -/// HTTP proxy configuration for sandboxed traffic. +/// Credential-free Connector catalog entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyNetworkProxy +public sealed class ConnectorCatalogEntry { - /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. - [JsonPropertyName("password")] - public string? Password { get; set; } + /// Untrusted service description, when present. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; + /// Untrusted display label from the service. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. - [JsonPropertyName("username")] - public string? Username { get; set; } -} + /// Optional catalog logo. + [JsonPropertyName("logo")] + public string? Logo { get; set; } -/// Network rules to merge into the base policy. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyNetwork -{ - /// Hosts allowed through the built-in sandbox proxy. A non-empty list denies unmatched hosts; an absent or empty list allows all hosts not blocked. Supports exact hostnames, IP addresses, and *.example.com for strict subdomains. Host rules do not override the outbound or local-network toggles. - [JsonPropertyName("allowedHosts")] - public IList? AllowedHosts { get; set; } + /// Canonical Connector name used by lifecycle methods. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Whether traffic to local/loopback addresses is allowed. - [JsonPropertyName("allowLocalNetwork")] - public bool? AllowLocalNetwork { get; set; } + /// Optional catalog release tag. + [JsonPropertyName("releaseTag")] + public string? ReleaseTag { get; set; } - /// Whether outbound network traffic is allowed at all. - [JsonPropertyName("allowOutbound")] - public bool? AllowOutbound { get; set; } + /// Opaque stable runtime IDs currently projected into the session for this Connector. + [JsonPropertyName("runtimeServerIds")] + public IList RuntimeServerIds { get => field ??= []; set; } - /// Hosts denied by the built-in sandbox proxy. Deny rules take precedence over allowedHosts. A domain also denies all its subdomains. IP addresses match exactly; *.example.com matches strict subdomains, and * denies every host. - [JsonPropertyName("blockedHosts")] - public IList? BlockedHosts { get; set; } + /// Current authoritative service connection state. + [JsonPropertyName("status")] + public ConnectorCatalogStatus Status { get; set; } - /// HTTP(S) proxy for sandboxed traffic. This is the built-in local proxy's upstream: every sandboxed command reaches it through a loopback listener, so credentials stay in the runtime and never reach the child. On Windows the sandbox also needs local network access, because it reaches that listener over host loopback. Configure credentials in the separate username/password fields. The transient local listener URL is never persisted. - [JsonPropertyName("proxy")] - public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } + /// Optional catalog tier. + [JsonPropertyName("tier")] + public string? Tier { get; set; } } -/// macOS seatbelt-specific options. +/// Validated Connector catalog snapshot cached by the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicySeatbelt +public sealed class ConnectorCatalogResult { - /// Whether the macOS seatbelt profile may access the keychain. - [JsonPropertyName("keychainAccess")] - public bool? KeychainAccess { get; set; } + /// Validated catalog entries in service order. + [JsonPropertyName("connectors")] + public IList Connectors { get => field ??= []; set; } + + /// Unix epoch milliseconds when this snapshot was accepted. + [JsonPropertyName("refreshedAtMs")] + public long RefreshedAtMs { get; set; } + + /// Monotonically increasing session-local catalog revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } } -/// User-managed sandbox policy fragment merged into the auto-discovered base policy. +/// Live status of one session-owned MCP projection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicy +public sealed class ConnectorRuntimeStatus { - /// Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. - [JsonPropertyName("experimental")] - public SandboxConfigUserPolicyExperimental? Experimental { get; set; } - - /// Filesystem rules to merge into the base policy. - [JsonPropertyName("filesystem")] - public SandboxConfigUserPolicyFilesystem? Filesystem { get; set; } + /// Canonical Connector name that owns this server. + [JsonPropertyName("connectorName")] + public string ConnectorName { get; set; } = string.Empty; - /// Network rules to merge into the base policy. - [JsonPropertyName("network")] - public SandboxConfigUserPolicyNetwork? Network { get; set; } + /// Opaque runtime server ID. + [JsonPropertyName("runtimeServerId")] + public string RuntimeServerId { get; set; } = string.Empty; - /// macOS seatbelt options to merge into the base policy. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicySeatbelt? Seatbelt { get; set; } + /// Current live MCP host status. + [JsonPropertyName("status")] + public ConnectorMcpStatus Status { get; set; } } -/// Resolved sandbox configuration. +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfig +public sealed class ConnectorStatus { - /// Whether to auto-add the current working directory to readwritePaths. Default: true. - [JsonPropertyName("addCurrentWorkingDirectory")] - public bool? AddCurrentWorkingDirectory { get; set; } - - /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). - [JsonPropertyName("allowBypass")] - public bool? AllowBypass { get; set; } + /// Opaque account selection pinned to this session, when one has been selected. + [JsonPropertyName("accountId")] + public string? AccountId { get; set; } - /// 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; } + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } - /// Credential-injection capability flags. - [JsonPropertyName("auth")] - public SandboxConfigAuth? Auth { get; set; } + /// Exact selected account and stable scope requiring an authorization update, when proven by the Connector service. + [JsonPropertyName("authorizationRequirement")] + public ConnectorAuthorizationRequirement? AuthorizationRequirement { get; set; } - /// Opt-in whole-value environment masking for sandboxed shell, MCP, and LSP children. Configured names get random sentinels; the local proxy substitutes them only in HTTPS request headers at their injection hosts. Approved bypasses skip masking and the sandbox proxy, so bypassed shells may receive the real environment values. Disabled or explicitly opted-out routes are not protected. No credential values are stored in this configuration. - [JsonPropertyName("credentials")] - public SandboxCredentialsConfig? Credentials { get; set; } + /// Current feature and session availability. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { get; set; } - /// Whether sandboxing is enabled for the session. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Latest validated catalog snapshot, when available. + [JsonPropertyName("catalog")] + public ConnectorCatalogResult? Catalog { get; set; } - /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. - [JsonInclude] - [JsonPropertyName("managedLspRoutingLocked")] - internal bool? ManagedLspRoutingLocked { get; set; } + /// Number of active opaque connection continuations. + [JsonPropertyName("pendingConnections")] + public long PendingConnections { get; set; } - /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. - [JsonInclude] - [JsonPropertyName("managedMcpRoutingLocked")] - internal bool? ManagedMcpRoutingLocked { get; set; } + /// Live MCP status for every Connector-owned runtime server. + [JsonPropertyName("runtimeServers")] + public IList RuntimeServers { get => field ??= []; set; } +} - /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). - [JsonPropertyName("sandboxLspServers")] - public bool? SandboxLspServers { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionConnectorsGetStatusRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). - [JsonPropertyName("sandboxMcpServers")] - public bool? SandboxMcpServers { get; set; } +/// Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ConnectorAccountRequest +{ + /// Opaque account selection ID previously returned by an account discovery API. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// User-managed sandbox policy fragment merged into the auto-discovered base policy. - [JsonPropertyName("userPolicy")] - public SandboxConfigUserPolicy? UserPolicy { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// -/// Command-scoped GitHub credential injection for the shell commands an agent runs. -/// -/// Each channel is opt-in and independent, and injection is scoped to the individual command -/// spawn: the credential is resolved from the session's *current* authentication at every spawn -/// and reaches only spawns whose script actually invokes `git` or `gh`. Because nothing is -/// retained between spawns, replacing the session credential (`session.gitHubAuth.setCredentials`) -/// changes what the next spawned command presents — which seeding a credential into the runtime -/// process's own environment cannot do, since a child's environment is fixed at `exec`. -/// -/// The credential is matched to the host it authenticates to, so a github.com credential is never -/// presented to a GitHub Enterprise host and vice versa. Where a channel cannot express that -/// boundary it injects nothing rather than crossing it -- see `gh` below. -/// -/// This is independent of `sandboxConfig`: it is a decision about which identity the agent -/// presents, not about what the agent may touch, and it works on every platform whether or not -/// an OS sandboxing backend is available. `sandboxConfig.auth` remains the sandbox-scoped -/// spelling and is additive with this one. -/// +/// Typed result of initiating or continuing a Connector connection. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellCredentials +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ConnectorConnectResultConnected), "connected")] +[JsonDerivedType(typeof(ConnectorConnectResultConsentRequired), "consent_required")] +[JsonDerivedType(typeof(ConnectorConnectResultPending), "pending")] +public partial class ConnectorConnectResult { - /// - /// Whether to authenticate the agent's `gh` commands as the session's GitHub credential, by - /// exporting `GH_TOKEN` to a spawn that runs `gh`. Any inherited `gh` credential is removed from - /// spawns that do not, so the credential stays command-scoped. - /// - /// Applies to a github.com credential only. `gh` picks its credential variable from the host a - /// command targets rather than the one the credential belongs to, and the command can choose that - /// target, so `GH_ENTERPRISE_TOKEN` would offer a single-tenant enterprise credential to every - /// other enterprise host. A session whose credential is enterprise-scoped therefore runs `gh` - /// unauthenticated; its `git` commands are unaffected, because `http.<host>.extraheader` is scoped - /// to one host by construction. Default: false (opt-in). - /// - [JsonPropertyName("gh")] - public bool? Gh { get; set; } - - /// - /// Whether to authenticate the agent's `git` commands as the session's GitHub credential, by - /// injecting an `http.<host>.extraheader` (plus `insteadOf` rewrites so SSH-spelled remotes for - /// that host use the authenticated HTTPS transport). Applied only to a spawn that runs a - /// remote-contacting `git` subcommand. Default: false (opt-in). - /// - [JsonPropertyName("git")] - public bool? Git { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// A host-provided script sourced before each built-in shell command when its shell target matches the active shell. + +/// The service is connected and the session MCP graph was reconciled. +/// The connected variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellInitScript +public partial class ConnectorConnectResultConnected : ConnectorConnectResult { - /// Path to the script to source. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "connected"; - /// Built-in shell that may source this script. - [JsonPropertyName("shell")] - public ShellInitScriptShell Shell { get; set; } + /// Fresh authoritative Connector state after MCP reconciliation. + [JsonPropertyName("status")] + public required ConnectorStatus Status { get; set; } } -/// Per-session settings for built-in shell tools. +/// Host-owned consent is required before bounded continuation can complete. +/// The consent_required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellOptions +public partial class ConnectorConnectResultConsentRequired : ConnectorConnectResult { - /// Command-scoped GitHub credential injection for shell commands. - [JsonPropertyName("credentials")] - public ShellCredentials? Credentials { get; set; } - - /// Controls automatic non-interactive profile loading where supported. Explicit initScripts are unaffected. - [JsonPropertyName("initProfile")] - public ShellInitProfile? InitProfile { get; set; } + /// + [JsonIgnore] + public override string Kind => "consent_required"; - /// - /// Ordered host-provided script paths sourced before each built-in shell command when the - /// entry's shell target matches the active shell. Use these for rc files, environment setup scripts, - /// or other custom scripts. A script that returns a nonzero status is reported, and later scripts - /// and the user command continue while the shell remains running. Because scripts are sourced into - /// the command shell, `exit`, `exec`, failures under `set -e`, or other shell-terminating behavior - /// can prevent continuation. Script standard output is preserved; Bash script stderr is discarded, - /// PowerShell exception messages are replaced, and runtime-generated failure notices omit - /// configured script paths. When sandboxing is enabled, each script must already be readable under - /// the active sandbox filesystem policy. Pass an empty array to clear the list. - /// - [JsonPropertyName("initScripts")] - public IList? InitScripts { get; set; } + /// Validated HTTPS consent URL. The runtime does not open it. + [JsonPropertyName("consentUrl")] + public required string ConsentUrl { get; set; } - /// - /// Flags passed to the active built-in shell process on startup, replacing its default flags. - /// When omitted, the built-in Bash shell uses `--norc --noprofile`, - /// and the built-in PowerShell shell uses `-NoProfile -NoLogo`. - /// - [JsonPropertyName("processFlags")] - public IList? ProcessFlags { get; set; } + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { get; set; } } -/// Patch of mutable session options to apply to the running session. +/// The service is still completing the connection without a consent URL. +/// The pending variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUpdateOptionsParams +public partial class ConnectorConnectResultPending : ConnectorConnectResult { - /// Additional content-exclusion policies to merge into the session's policy set. - [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] - [JsonPropertyName("additionalContentExclusionPolicies")] - public IList? AdditionalContentExclusionPolicies { get; set; } - - /// Runtime context discriminator (e.g., `cli`, `actions`). - [JsonPropertyName("agentContext")] - public string? AgentContext { get; set; } - - /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. - [JsonPropertyName("allowAllMcpServerInstructions")] - public bool? AllowAllMcpServerInstructions { get; set; } - - /// Whether to disable the `ask_user` tool (encourages autonomous behavior). - [JsonPropertyName("askUserDisabled")] - public bool? AskUserDisabled { get; set; } - - /// Allowlist of tool names available to this session. - [JsonPropertyName("availableTools")] - public IList? AvailableTools { get; set; } - - /// Options scoped to the built-in CAPI (Copilot API) provider. - [JsonPropertyName("capi")] - public CapiSessionOptions? Capi { get; set; } - - /// Identifier of the client driving the session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } - - /// Whether to include the `Co-authored-by` trailer in commit messages. - [JsonPropertyName("coauthorEnabled")] - public bool? CoauthorEnabled { get; set; } - - /// Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. - [JsonPropertyName("contextTier")] - public OptionsUpdateContextTier? ContextTier { get; set; } - - /// Whether to allow auto-mode continuation across turns. - [JsonPropertyName("continueOnAutoMode")] - public bool? ContinueOnAutoMode { get; set; } - - /// Override URL for the Copilot API endpoint. - [JsonPropertyName("copilotUrl")] - public string? CopilotUrl { get; set; } - - /// Whether to default custom agents to local-only execution. - [JsonPropertyName("customAgentsLocalOnly")] - public bool? CustomAgentsLocalOnly { get; set; } - - /// Instruction source IDs to exclude from the system prompt. - [JsonPropertyName("disabledInstructionSources")] - public IList? DisabledInstructionSources { get; set; } - - /// Skill IDs that should be excluded from this session. - [JsonPropertyName("disabledSkills")] - public IList? DisabledSkills { get; set; } - - /// Whether to enable loading of `.github/hooks/` filesystem hooks. Separate from the SDK callback hook mechanism. - [JsonPropertyName("enableFileHooks")] - public bool? EnableFileHooks { get; set; } - - /// Whether to enable host git operations (context resolution, child repo scanning, git info in system prompt). - [JsonPropertyName("enableHostGitOperations")] - public bool? EnableHostGitOperations { get; set; } - - /// Whether to discover custom instructions on demand after successful file views (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md surfacing). Combined with `skipCustomInstructions`. - [JsonPropertyName("enableOnDemandInstructionDiscovery")] - public bool? EnableOnDemandInstructionDiscovery { get; set; } - - /// Whether to surface reasoning-summary events from the model. - [JsonPropertyName("enableReasoningSummaries")] - public bool? EnableReasoningSummaries { get; set; } - - /// Whether shell-script safety heuristics are enabled. - [JsonPropertyName("enableScriptSafety")] - public bool? EnableScriptSafety { get; set; } - - /// Whether to enable cross-session store writes and reads. - [JsonPropertyName("enableSessionStore")] - public bool? EnableSessionStore { get; set; } - - /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. - [JsonPropertyName("enableSkills")] - public bool? EnableSkills { get; set; } - - /// Whether to stream model responses. - [JsonPropertyName("enableStreaming")] - public bool? EnableStreaming { get; set; } - - /// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). - [JsonPropertyName("envValueMode")] - public OptionsUpdateEnvValueMode? EnvValueMode { get; set; } - - /// Override directory for the session-events log. When unset, the runtime's default events log directory is used. - [JsonPropertyName("eventsLogDirectory")] - public string? EventsLogDirectory { get; set; } - - /// Whether subagent callback events should be forwarded into the session event log sink. - [JsonPropertyName("eventsLogIncludesSubagents")] - public bool? EventsLogIncludesSubagents { get; set; } - - /// Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. - [JsonPropertyName("excludedBuiltinAgents")] - public IList? ExcludedBuiltinAgents { get; set; } - - /// Denylist of tool names for this session. - [JsonPropertyName("excludedTools")] - public IList? ExcludedTools { get; set; } - - /// Map of feature-flag IDs to their boolean enabled state. - [JsonPropertyName("featureFlags")] - public IDictionary? FeatureFlags { get; set; } - - /// Skill scan directories and descendants excluded from discovery. Supports `~`-relative paths. - [JsonPropertyName("ignoredSkillsLocations")] - public IList? IgnoredSkillsLocations { get; set; } - - /// Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. Set to null to remove the allowlist restriction. - [JsonPropertyName("includedBuiltinAgents")] - public IList? IncludedBuiltinAgents { get; set; } - - /// 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; } - - /// Stable integration identifier used for analytics and rate-limit attribution. - [JsonPropertyName("integrationId")] - public string? IntegrationId { get; set; } - - /// Whether experimental capabilities are enabled. - [JsonPropertyName("isExperimentalMode")] - public bool? IsExperimentalMode { get; set; } - - /// Whether interactive shell sessions are logged. - [JsonPropertyName("logInteractiveShells")] - public bool? LogInteractiveShells { get; set; } - - /// Identifier sent to LSP-style integrations. - [JsonPropertyName("lspClientName")] - public string? LspClientName { get; set; } - - /// Whether to expose the `manage_schedule` tool to the agent. The runtime always owns the per-session schedule registry; this flag only controls tool exposure (typically gated to staff users). - [JsonPropertyName("manageScheduleEnabled")] - public bool? ManageScheduleEnabled { get; set; } - - /// 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. - [JsonPropertyName("maxInlineBinaryBytes")] - public long? MaxInlineBinaryBytes { get; set; } - - /// The model ID to use for assistant turns. - [JsonPropertyName("model")] - public string? Model { get; set; } - - /// Per-property model capability overrides for the selected model. - [JsonPropertyName("modelCapabilitiesOverrides")] - public ModelCapabilitiesOverride? ModelCapabilitiesOverrides { get; set; } + /// + [JsonIgnore] + public override string Kind => "pending"; - /// Organization-level custom instructions to inject into the system prompt. - [JsonPropertyName("organizationCustomInstructions")] - public string? OrganizationCustomInstructions { get; set; } + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { get; set; } +} - /// Custom model-provider configuration (BYOK). - [JsonPropertyName("provider")] - public ProviderConfig? Provider { get; set; } +/// Selects one Connector and the pinned host-owned account used for its service and MCP authorization. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ConnectorConnectRequest +{ + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// Reasoning effort for the selected model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. When omitted, no effort override is applied. - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + /// Canonical Connector name from the current catalog. + [JsonPropertyName("connectorName")] + public string ConnectorName { get; set; } = string.Empty; - /// Reasoning summary mode for supported model clients. - [JsonPropertyName("reasoningSummary")] - public OptionsUpdateReasoningSummary? ReasoningSummary { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether the session is running in an interactive UI. - [JsonPropertyName("runningInInteractiveMode")] - public bool? RunningInInteractiveMode { get; set; } +/// Explicitly bounded continuation of a pending Connector connection. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ConnectorContinueRequest +{ + /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + [JsonPropertyName("continuationId")] + public string ContinuationId { get; set; } = string.Empty; - /// Resolved sandbox configuration. - [JsonPropertyName("sandboxConfig")] - public SandboxConfig? SandboxConfig { get; set; } + /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. + [JsonPropertyName("deadlineMs")] + public int DeadlineMs { get; set; } - /// Origin of the sandbox choice. Settings-derived origins (never_configured, user_enabled, user_disabled, repository_policy) let managed policy floor a host preference; explicit below-floor changes remain policy conflicts unless a session opt-out is authorized. Also used for telemetry provenance. - [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] - [JsonPropertyName("sandboxConfigSource")] - public SandboxConfigSource? SandboxConfigSource { get; set; } + /// Maximum catalog requests made by this call. Must be between one and the capability limit. + [JsonPropertyName("maxAttempts")] + public int MaxAttempts { 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; } + /// Delay in milliseconds between attempts. Must not exceed the capability limit. + [JsonPropertyName("pollIntervalMs")] + public int PollIntervalMs { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// Optional session limits. Pass null to clear the session limits. - [JsonPropertyName("sessionLimits")] - public SessionLimitsConfig? SessionLimits { get; set; } - - /// Per-session settings for built-in shell tools. - [JsonPropertyName("shell")] - public ShellOptions? Shell { get; set; } - - /// Use shell.initProfile instead. Shell init profile (`None` or `NonInteractive`). - [EditorBrowsable(EditorBrowsableState.Never)] -#if NET5_0_OR_GREATER - [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] -#endif - [JsonPropertyName("shellInitProfile")] - public string? ShellInitProfile { get; set; } - - /// PowerShell process flags applied to built-in and user-requested shell commands. - [JsonPropertyName("shellProcessFlags")] - public IList? ShellProcessFlags { get; set; } +/// Authoritative result after disconnect and MCP reconciliation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorDisconnectResult +{ + /// Whether the service accepted the idempotent disconnect. + [JsonPropertyName("disconnected")] + public bool Disconnected { get; set; } - /// Additional directories to search for skills. - [JsonPropertyName("skillDirectories")] - public IList? SkillDirectories { get; set; } + /// Fresh authoritative session state after removing Connector-owned MCP servers. + [JsonPropertyName("status")] + public ConnectorStatus Status { get => field ??= new(); set; } +} - /// Whether to skip loading custom instruction sources. - [JsonPropertyName("skipCustomInstructions")] - public bool? SkipCustomInstructions { get; set; } +/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorReconcileRequest +{ + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + [JsonPropertyName("accountId")] + public required string AccountId { get; set; } - /// Whether to skip embedding retrieval pipeline initialization and execution. - [JsonPropertyName("skipEmbeddingRetrieval")] - public bool? SkipEmbeddingRetrieval { get; set; } + /// Optional Connector name to reinitialize. Requires the targetedReconcile capability. + [JsonPropertyName("forceConnectorName")] + public string? ForceConnectorName { get; set; } - /// When true, the selected custom agent's prompt is not injected into the user message (skill context is still injected). Used by automation triggers where the agent prompt is already in the problem statement. - [JsonPropertyName("suppressCustomAgentPrompt")] - public bool? SuppressCustomAgentPrompt { get; set; } + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + [JsonPropertyName("refreshCatalog")] + public bool? RefreshCatalog { get; set; } +} - /// Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. - [JsonPropertyName("toolFilterPrecedence")] - public OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence { get; set; } +/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ConnectorReconcileRequestWithSession +{ + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// Optional path for trajectory output. - [JsonPropertyName("trajectoryFile")] - public string? TrajectoryFile { get; set; } + /// Optional Connector name to reinitialize. Requires the targetedReconcile capability. + [JsonPropertyName("forceConnectorName")] + public string? ForceConnectorName { get; set; } - /// Output verbosity level for supported models. - [JsonPropertyName("verbosity")] - public Verbosity? Verbosity { get; set; } + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + [JsonPropertyName("refreshCatalog")] + public bool? RefreshCatalog { get; set; } - /// Absolute working-directory path for shell tools. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Parameters for (re)loading the merged LSP configuration set. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class LspInitializeRequest +internal sealed class SessionConnectorsWithdrawProjectionRequest { - /// Force re-initialization even when LSP configs were already loaded for the working directory. - [JsonPropertyName("force")] - public bool? Force { get; set; } - - /// Git root used as the boundary when traversing for project-level LSP configs (supports monorepos). - [JsonPropertyName("gitRoot")] - public string? GitRoot { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// Working directory used to load project-level LSP configs. Defaults to the session working directory when omitted. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionManagedSettingsGetRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Discovered extension metadata, including source-qualified ID, name, discovery source, status, and optional process ID. +/// Session plugin metadata, with name, marketplace, optional version, and enabled state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Extension +public sealed class Plugin { - /// Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper', 'plugin:my-plugin:my-ext'). - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Opaque stable identity for a direct plugin source. + [JsonPropertyName("directSourceId")] + public string? DirectSourceId { get; set; } - /// Extension name (directory name). + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Whether this managed desired plugin has an installed or live record. + [JsonPropertyName("installed")] + public bool? Installed { get; set; } + + /// Absolute marketplace directory for a live plugin. + [JsonPropertyName("installedFrom")] + public string? InstalledFrom { get; set; } + + /// Whether enterprise managed settings control this plugin. + [JsonPropertyName("managed")] + public bool? Managed { get; set; } + + /// Enabled state required by enterprise managed settings. + [JsonPropertyName("managedDesiredEnabled")] + public bool? ManagedDesiredEnabled { get; set; } + + /// Marketplace the plugin came from. + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; + + /// Plugin name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Process ID if the extension is running. - [JsonPropertyName("pid")] - public long? Pid { get; set; } - - /// Discovery source: project (.github/extensions/), user (~/.copilot/extensions/), plugin (installed plugin), or session (session-state/<id>/extensions/). + /// Runtime plugin provenance, such as "builtin". [JsonPropertyName("source")] - public ExtensionSource Source { get; set; } + public string? Source { get; set; } - /// Current status: running, disabled, failed, or starting. - [JsonPropertyName("status")] - public ExtensionStatus Status { get; set; } + /// Installed version. + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Extensions discovered for the session, with their current status. +/// Plugins installed for the session, with their enabled state and version metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExtensionList +public sealed class PluginList { - /// Discovered extensions and their current status. - [JsonPropertyName("extensions")] - public IList Extensions { get => field ??= []; set; } + /// Installed plugins. + [JsonPropertyName("plugins")] + public IList Plugins { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionExtensionsListRequest +internal sealed class SessionPluginsListRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Source-qualified extension identifier to enable for the session. +/// Plugin source resolved relative to the session's authoritative working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ExtensionsEnableRequest +internal sealed class SessionPluginsInstallRequest { - /// Source-qualified extension ID to enable. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Plugin install spec. Accepts the same forms as the CLI: "plugin@marketplace" (marketplace install), "owner/repo" or "owner/repo:subpath" (GitHub direct), an http/https/ssh URL, or a local path. Direct (non-marketplace) installs are deprecated and will produce a deprecationWarning in the result. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; } -/// Source-qualified extension identifier to disable for the session. +/// Name (or spec) of the plugin to uninstall. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ExtensionsDisableRequest +internal sealed class PluginsUninstallRequestWithSession { - /// Source-qualified extension ID to disable. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - + /// Stable source identity for a direct (non-marketplace) install. Disambiguates uninstall when multiple installed plugins share the same name. + [JsonPropertyName("directSourceId")] + public string? DirectSourceId { get; set; } + + /// Plugin name or "plugin@marketplace" spec to uninstall. When ambiguous, prefer the fully-qualified spec. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Name (or spec) of the plugin to update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionExtensionsReloadRequest +internal sealed class PluginsUpdateRequestWithSession { + /// Plugin name or "plugin@marketplace" spec to update. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. -/// Polymorphic base type discriminated by type. +/// Plugin names (or specs) to enable in the session's authoritative working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(PushAttachmentFile), "file")] -[JsonDerivedType(typeof(PushAttachmentDirectory), "directory")] -[JsonDerivedType(typeof(PushAttachmentSelection), "selection")] -[JsonDerivedType(typeof(PushAttachmentGitHubReference), "github_reference")] -[JsonDerivedType(typeof(PushAttachmentGitHubCommit), "github_commit")] -[JsonDerivedType(typeof(PushAttachmentGitHubRelease), "github_release")] -[JsonDerivedType(typeof(PushAttachmentGitHubActionsJob), "github_actions_job")] -[JsonDerivedType(typeof(PushAttachmentGitHubRepository), "github_repository")] -[JsonDerivedType(typeof(PushAttachmentGitHubFileDiff), "github_file_diff")] -[JsonDerivedType(typeof(PushAttachmentGitHubTreeComparison), "github_tree_comparison")] -[JsonDerivedType(typeof(PushAttachmentGitHubUrl), "github_url")] -[JsonDerivedType(typeof(PushAttachmentGitHubFile), "github_file")] -[JsonDerivedType(typeof(PushAttachmentGitHubSnippet), "github_snippet")] -[JsonDerivedType(typeof(PushAttachmentBlob), "blob")] -[JsonDerivedType(typeof(PushAttachmentExtensionContext), "extension_context")] -public partial class PushAttachment +internal sealed class SessionPluginsEnableRequest { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} + /// Plugin names or "plugin@marketplace" specs to enable. Unknown names are ignored. Non-marketplace direct installs are always enabled and cannot be toggled via this API. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} -/// Optional line range to scope the attachment to a specific section of the file. +/// Plugin names (or specs) to disable in the session's authoritative working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentFileLineRange +internal sealed class SessionPluginsDisableRequest { - /// End line number (1-based, inclusive). - [JsonPropertyName("end")] - public long End { get; set; } + /// Plugin names or "plugin@marketplace" specs to disable. Unknown names are ignored. Non-marketplace direct installs cannot be disabled via this API; uninstall them instead. Plugin-owned MCP servers are stopped in active sessions immediately; other plugin contributions remain available until each session reloads plugins. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } - /// Start line number (1-based). - [JsonPropertyName("start")] - public long Start { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// File attachment. -/// The file variant of . +/// RPC data type for SessionPluginsReload operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentFile : PushAttachment +public sealed class SessionPluginsReloadRequest { - /// - [JsonIgnore] - public override string Type => "file"; + /// When true, skip repo-level hooks during the hook reload. Use before folder trust is confirmed; load them post-trust via `sessions.loadDeferredRepoHooks`. + [JsonPropertyName("deferRepoHooks")] + public bool? DeferRepoHooks { get; set; } - /// User-facing display name for the attachment. - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } + /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. + [JsonPropertyName("reloadCustomAgents")] + public bool? ReloadCustomAgents { get; set; } - /// Optional line range to scope the attachment to a specific section of the file. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("lineRange")] - public PushAttachmentFileLineRange? LineRange { get; set; } + /// Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). + [JsonPropertyName("reloadExtensions")] + public bool? ReloadExtensions { get; set; } - /// Absolute file path. - [JsonPropertyName("path")] - public required string Path { get; set; } + /// Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). + [JsonPropertyName("reloadHooks")] + public bool? ReloadHooks { get; set; } + + /// Reload MCP server connections after refreshing plugins. Defaults to true. + [JsonPropertyName("reloadMcp")] + public bool? ReloadMcp { get; set; } } -/// Directory attachment. -/// The directory variant of . +/// RPC data type for SessionPluginsReloadRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentDirectory : PushAttachment +internal sealed class SessionPluginsReloadRequestWithSession { - /// - [JsonIgnore] - public override string Type => "directory"; - - /// User-facing display name for the attachment. - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } + /// When true, skip repo-level hooks during the hook reload. Use before folder trust is confirmed; load them post-trust via `sessions.loadDeferredRepoHooks`. + [JsonPropertyName("deferRepoHooks")] + public bool? DeferRepoHooks { get; set; } - /// Absolute directory path. - [JsonPropertyName("path")] - public required string Path { get; set; } -} + /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. + [JsonPropertyName("reloadCustomAgents")] + public bool? ReloadCustomAgents { get; set; } -/// End position of the selection. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentSelectionDetailsEnd -{ - /// End character offset within the line (0-based). - [JsonPropertyName("character")] - public long Character { get; set; } + /// Re-discover and relaunch subprocess extensions (including plugin-shipped extensions) after refreshing plugins. Defaults to true. Has no effect when the session has no active extension controller (e.g. extensions were not requested for the session). + [JsonPropertyName("reloadExtensions")] + public bool? ReloadExtensions { get; set; } - /// End line number (0-based). - [JsonPropertyName("line")] - public long Line { get; set; } -} + /// Re-load user, plugin, and (subject to `deferRepoHooks`) repo hooks. Defaults to true. Has no effect when the host has not registered a hook reloader (e.g. remote sessions). + [JsonPropertyName("reloadHooks")] + public bool? ReloadHooks { get; set; } -/// Start position of the selection. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentSelectionDetailsStart -{ - /// Start character offset within the line (0-based). - [JsonPropertyName("character")] - public long Character { get; set; } + /// Reload MCP server connections after refreshing plugins. Defaults to true. + [JsonPropertyName("reloadMcp")] + public bool? ReloadMcp { get; set; } - /// Start line number (0-based). - [JsonPropertyName("line")] - public long Line { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Position range of the selection within the file. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentSelectionDetails +internal sealed class SessionPluginsMarketplacesListRequest { - /// End position of the selection. - [JsonPropertyName("end")] - public PushAttachmentSelectionDetailsEnd End { get => field ??= new(); set; } - - /// Start position of the selection. - [JsonPropertyName("start")] - public PushAttachmentSelectionDetailsStart Start { get => field ??= new(); set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Code selection attachment from an editor. -/// The selection variant of . +/// Marketplace source and optional working directory for relative-path resolution. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentSelection : PushAttachment +internal sealed class PluginsMarketplacesAddRequestWithSession { - /// - [JsonIgnore] - public override string Type => "selection"; - - /// User-facing display name for the selection. - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } - - /// Absolute path to the file containing the selection. - [JsonPropertyName("filePath")] - public required string FilePath { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Position range of the selection within the file. - [JsonPropertyName("selection")] - public required PushAttachmentSelectionDetails Selection { get; set; } + /// Marketplace source. Accepts the same forms as the CLI: "owner/repo" or "owner/repo#ref" (GitHub), an http/https/ssh URL (optionally with #ref), a git scp-style URL (user@host:path), or a local path. The marketplace's own name (from its manifest) is used as the registration key. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; - /// The selected text content. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// Working directory used to resolve relative local paths in `source`. Defaults to the server's current working directory. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// GitHub issue, pull request, or discussion reference. -/// The github_reference variant of . +/// Name of the marketplace to remove and an optional force flag. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubReference : PushAttachment +internal sealed class PluginsMarketplacesRemoveRequestWithSession { - /// - [JsonIgnore] - public override string Type => "github_reference"; - - /// Issue, pull request, or discussion number. - [JsonPropertyName("number")] - public required long Number { get; set; } - - /// Type of GitHub reference. - [JsonPropertyName("referenceType")] - public required PushAttachmentGitHubReferenceType ReferenceType { get; set; } - - /// Current state of the referenced item (e.g., open, closed, merged). - [JsonPropertyName("state")] - public required string State { get; set; } + /// When true, also uninstall every plugin sourced from this marketplace. When false (default), removal is a no-op if any plugin from this marketplace is installed and the dependent plugin names are returned in the result. + [JsonPropertyName("force")] + public bool? Force { get; set; } - /// Title of the referenced item. - [JsonPropertyName("title")] - public required string Title { get; set; } + /// Marketplace name to remove. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// URL to the referenced item on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Pointer to a GitHub repository. +/// Name of the marketplace whose plugin catalog to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushGitHubRepoRef +internal sealed class PluginsMarketplacesBrowseRequestWithSession { - /// Numeric GitHub repository id. - [JsonPropertyName("id")] - public long? Id { get; set; } - - /// Repository name (without owner). + /// Marketplace name to browse. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Repository owner login (user or organization). - [JsonPropertyName("owner")] - public string Owner { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Pointer to a GitHub commit. -/// The github_commit variant of . +/// RPC data type for SessionPluginsMarketplacesRefresh operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubCommit : PushAttachment +public sealed class SessionPluginsMarketplacesRefreshRequest { - /// - [JsonIgnore] - public override string Type => "github_commit"; - - /// First line of the commit message. - [JsonPropertyName("message")] - public required string Message { get; set; } - - /// Full commit SHA. - [JsonPropertyName("oid")] - public required string Oid { get; set; } - - /// Repository the commit belongs to. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } - - /// URL to the commit on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. + [JsonPropertyName("name")] + public string? Name { get; set; } } -/// Pointer to a GitHub release. -/// The github_release variant of . +/// RPC data type for SessionPluginsMarketplacesRefreshRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubRelease : PushAttachment +internal sealed class SessionPluginsMarketplacesRefreshRequestWithSession { - /// - [JsonIgnore] - public override string Type => "github_release"; - - /// Human-readable release name. + /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. [JsonPropertyName("name")] - public required string Name { get; set; } - - /// Repository the release belongs to. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } - - /// Git tag the release is anchored to. - [JsonPropertyName("tagName")] - public required string TagName { get; set; } + public string? Name { get; set; } - /// URL to the release on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Pointer to a GitHub Actions job. -/// The github_actions_job variant of . +/// Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubActionsJob : PushAttachment +public sealed class ProviderSessionToken { - /// - [JsonIgnore] - public override string Type => "github_actions_job"; - - /// Terminal conclusion of the job when finished (e.g., success, failure, cancelled). Absent for in-progress jobs. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("conclusion")] - public string? Conclusion { get; set; } - - /// Job id within the workflow run. - [JsonPropertyName("jobId")] - public required long JobId { get; set; } - - /// Display name of the job. - [JsonPropertyName("jobName")] - public required string JobName { get; set; } + /// When the token expires, if known. Callers should refresh by calling `getEndpoint` again before this time, or reactively on any 401/403 response from `baseUrl`. + [JsonPropertyName("expiresAt")] + public DateTimeOffset? ExpiresAt { get; set; } - /// Repository the workflow run belongs to. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// HTTP header name the token must be sent under. + [JsonPropertyName("header")] + public string Header { get; set; } = string.Empty; - /// URL to the job on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// The model the token is bound to, when applicable. When set, the token is only valid for requests against this model. + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Display name of the workflow the job ran in. - [JsonPropertyName("workflowName")] - public required string WorkflowName { get; set; } + /// The short-lived token value. + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; } -/// Pointer to a GitHub repository. -/// The github_repository variant of . +/// A snapshot of the provider endpoint the session is currently configured to talk to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubRepository : PushAttachment +public sealed class ProviderEndpoint { - /// - [JsonIgnore] - public override string Type => "github_repository"; + /// A credential the caller should use with this endpoint. Omitted only when the endpoint accepts unauthenticated requests. + [JsonPropertyName("apiKey")] + public string? ApiKey { get; set; } - /// Short description of the repository. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Base URL to pass to the LLM client library. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = string.Empty; - /// Git ref this attachment is anchored at (branch, tag, or commit). When absent the default branch is implied. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("ref")] - public string? Ref { get; set; } + /// HTTP headers the caller must include on every outbound request. + [JsonPropertyName("headers")] + public IDictionary Headers { get => field ??= new Dictionary(); set; } - /// Repository pointer. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one. + [JsonPropertyName("sessionToken")] + public ProviderSessionToken? SessionToken { get; set; } - /// URL to the repository on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Transport to be used for provider requests. + [JsonPropertyName("transport")] + public ProviderEndpointTransport? Transport { get; set; } + + /// Provider family. Matches the `type` field of a BYOK provider config. + [JsonPropertyName("type")] + public ProviderEndpointType Type { get; set; } + + /// Wire API to be used, when required for the provider type. + [JsonPropertyName("wireApi")] + public ProviderEndpointWireApi? WireApi { get; set; } } -/// One side of a file diff (head or base). +/// RPC data type for SessionProviderGetEndpoint operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentGitHubFileDiffSide +public sealed class SessionProviderGetEndpointRequest { - /// Repository-relative path to the file. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Git ref (branch, tag, or commit SHA) the file is read at. - [JsonPropertyName("ref")] - public string Ref { get; set; } = string.Empty; - - /// Repository the file lives in. - [JsonPropertyName("repo")] - public PushGitHubRepoRef Repo { get => field ??= new(); set; } + /// Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } } -/// Pointer to a single-file diff. At least one of `head` and `base` must be present. -/// The github_file_diff variant of . +/// RPC data type for SessionProviderGetEndpointRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubFileDiff : PushAttachment +internal sealed class SessionProviderGetEndpointRequestWithSession { - /// - [JsonIgnore] - public override string Type => "github_file_diff"; - - /// File location on the base side of the diff. Absent for additions. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("base")] - public PushAttachmentGitHubFileDiffSide? Base { get; set; } - - /// File location on the head side of the diff. Absent for deletions. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("head")] - public PushAttachmentGitHubFileDiffSide? Head { get; set; } + /// Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } - /// URL to the diff on GitHub (e.g., a commit, compare, or PR-file URL). - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// One side of a tree comparison (head or base). +/// The selectable model entries synthesized for the models added by this call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentGitHubTreeComparisonSide +public sealed class ProviderAddResult { - /// Repository the revision belongs to. - [JsonPropertyName("repo")] - public PushGitHubRepoRef Repo { get => field ??= new(); set; } - - /// Git revision (branch, tag, or commit SHA). - [JsonPropertyName("revision")] - public string Revision { get; set; } = string.Empty; + /// Synthesized selectable model entries for the newly added BYOK models, each under its provider-qualified selection id (`provider/id`). Empty when only providers were added. + [JsonPropertyName("models")] + public IList Models { get => field ??= []; set; } } -/// Pointer to a comparison between two git revisions. -/// The github_tree_comparison variant of . +/// BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubTreeComparison : PushAttachment +internal sealed class ProviderAddRequest { - /// - [JsonIgnore] - public override string Type => "github_tree_comparison"; - - /// Base side of the comparison. - [JsonPropertyName("base")] - public required PushAttachmentGitHubTreeComparisonSide Base { get; set; } + /// BYOK model definitions to register. Each must reference a provider that is already registered or included in this same call. Selection ids (`provider/id`) must be unique across the registry. + [JsonPropertyName("models")] + public IList? Models { get; set; } - /// Head side of the comparison. - [JsonPropertyName("head")] - public required PushAttachmentGitHubTreeComparisonSide Head { get; set; } + /// Named BYOK provider connections to register, additive to any providers already in the registry. Each name must be unique across the registry and must not contain '/'. + [JsonPropertyName("providers")] + public IList? Providers { get; set; } - /// URL to the comparison on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Generic GitHub URL reference. -/// The github_url variant of . +/// The selectable model entries and selection ids synthesized for the synchronized BYOK models. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubUrl : PushAttachment +public sealed class ProviderSyncResult { - /// - [JsonIgnore] - public override string Type => "github_url"; + /// True when synchronization withdrew the selected host-managed model, leaving the session with no explicit selection, so ordinary model resolution picks the session default. Synchronization never promotes a surviving host model in its place: publishing a model offers it, and the choice of which model to use stays with the user. + [JsonPropertyName("modelDeselected")] + public bool? ModelDeselected { get; set; } - /// URL to the GitHub resource. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Synthesized selectable model entries for the synchronized BYOK models. + [JsonPropertyName("models")] + public IList Models { get => field ??= []; set; } + + /// Provider-qualified model selection ids present after synchronization. + [JsonPropertyName("selectionIds")] + public IList SelectionIds { get => field ??= []; set; } } -/// Pointer to a file in a GitHub repository at a specific ref. -/// The github_file variant of . +/// Authoritative BYOK provider and model registry snapshot to apply atomically to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubFile : PushAttachment +internal sealed class ProviderSyncRequest { - /// - [JsonIgnore] - public override string Type => "github_file"; + /// BYOK model definition snapshot. Models absent from this list are removed. + [JsonPropertyName("models")] + public IList? Models { get; set; } - /// Repository-relative path to the file. - [JsonPropertyName("path")] - public required string Path { get; set; } - - /// Git ref the file is read at (branch, tag, or commit SHA). - [JsonPropertyName("ref")] - public required string Ref { get; set; } - - /// Repository the file lives in. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// Named BYOK provider connection snapshot. Providers absent from this list are removed. + [JsonPropertyName("providers")] + public IList? Providers { get; set; } - /// URL to the file on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Pointer to a line range inside a file in a GitHub repository. -/// The github_snippet variant of . +/// What the withdrawal actually removed from the registry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubSnippet : PushAttachment +public sealed class ProviderWithdrawResult { - /// - [JsonIgnore] - public override string Type => "github_snippet"; - - /// Line range the snippet covers. - [JsonPropertyName("lineRange")] - public required PushAttachmentFileLineRange LineRange { get; set; } - - /// Repository-relative path to the file. - [JsonPropertyName("path")] - public required string Path { get; set; } - - /// Git ref the file is read at (branch, tag, or commit SHA). - [JsonPropertyName("ref")] - public required string Ref { get; set; } + /// True when withdrawal removed the selected host-managed model, leaving the session with no explicit selection, so ordinary model resolution picks the session default. Withdrawal never promotes a surviving model in its place: the choice of which model to use stays with the user. + [JsonPropertyName("modelDeselected")] + public bool? ModelDeselected { get; set; } - /// Repository the file lives in. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + /// Providers removed because one of the withdrawn models was the last entry referencing them. A provider that merely has no models is not removed. + [JsonPropertyName("providersRemoved")] + public IList ProvidersRemoved { get => field ??= []; set; } - /// URL to the snippet on GitHub (with line anchor). - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Selection ids that were registered and are now withdrawn. Excludes requested ids that were not present. + [JsonPropertyName("withdrawn")] + public IList Withdrawn { get => field ??= []; set; } } -/// Blob attachment with inline base64-encoded data. -/// The blob variant of . +/// Host-managed model selection ids to withdraw from the session's BYOK registry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentBlob : PushAttachment +internal sealed class ProviderWithdrawRequest { - /// - [JsonIgnore] - public override string Type => "blob"; - - /// Base64-encoded content. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } - - /// User-facing display name for the attachment. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Provider-qualified selection ids to withdraw. Ids that are not registered are ignored, so withdrawal is idempotent. A provider left with no models referencing it is removed too. + [JsonPropertyName("models")] + public IList Models { get => field ??= []; set; } - /// MIME type of the inline data. - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Slim input shape for extension_context attachments; identity fields are runtime-derived. -/// The extension_context variant of . +/// Indicates whether the session options patch was applied successfully. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentExtensionContext : PushAttachment +public sealed class SessionUpdateOptionsResult { - /// - [JsonIgnore] - public override string Type => "extension_context"; - - /// Caller-supplied JSON payload (required, may be null but not undefined). - [JsonPropertyName("payload")] - public required JsonElement Payload { get; set; } + /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated. + [JsonPropertyName("pluginHookCount")] + public long? PluginHookCount { get; set; } - /// Human-readable composer pill label. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("title")] - public required string Title { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Parameters for session.extensions.sendAttachmentsToMessage. +/// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SendAttachmentsToMessageParams +public sealed class OptionsUpdateAdditionalContentExclusionPolicyRuleSource { - /// Attachments to push into the next user-message turn. extension_context entries take the slim shape; standard variants take their full AttachmentSchema shape. - [JsonPropertyName("attachments")] - public IList Attachments { get => field ??= []; set; } - - /// Optional canvas instance binding the push for provenance. When supplied, the runtime resolves the canvas, verifies it is owned by the calling extension, and stamps canvasId/instanceId onto each extension_context entry. When omitted, no resolution runs and those fields stay unset on the attachment. - [JsonPropertyName("instanceId")] - public string? InstanceId { get; set; } + /// Name of the policy source. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Type of the policy source. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; } -/// A tool name and arguments to execute through the session's native invocation pipeline. +/// Single content-exclusion rule supplied to `session.options.update`, with paths, match conditions, and source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsExecuteRequest +public sealed class OptionsUpdateAdditionalContentExclusionPolicyRule { - /// Arguments supplied to the tool. - [JsonPropertyName("arguments")] - public JsonElement Arguments { get; set; } + /// Conditions of which at least one must match. + [JsonPropertyName("ifAnyMatch")] + public IList? IfAnyMatch { get; set; } - /// Name of the currently offered tool to execute. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Conditions none of which may match. + [JsonPropertyName("ifNoneMatch")] + public IList? IfNoneMatch { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Path patterns covered by this rule. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } - /// Optional identifier used to correlate this invocation with its tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } + /// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. + [JsonPropertyName("source")] + public OptionsUpdateAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } } -/// Custom grammar input format accepted by a built-in tool. +/// Content-exclusion policy supplied to `session.options.update`, with rules, last-updated data, and scope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolFormat +public sealed class OptionsUpdateAdditionalContentExclusionPolicy { - /// Grammar definition accepted by the tool. - [JsonPropertyName("definition")] - public string Definition { get; set; } = string.Empty; + /// Opaque policy update timestamp supplied by the host. + [JsonPropertyName("last_updated_at")] + public JsonElement LastUpdatedAt { get; set; } - /// Grammar syntax used by the format definition. - [JsonPropertyName("syntax")] - public string Syntax { get; set; } = string.Empty; + /// Content-exclusion rules to apply. + [JsonPropertyName("rules")] + public IList Rules { get => field ??= []; set; } - /// Custom input-format discriminator. - [JsonPropertyName("type")] - public BuiltinToolFormatType Type { get; set; } + /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. + [JsonPropertyName("scope")] + public OptionsUpdateAdditionalContentExclusionPolicyScope Scope { get; set; } } -/// JSON Schema object accepted by a built-in tool. +/// Options scoped to the built-in CAPI (Copilot API) provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolInputSchema +public sealed class CapiSessionOptions { - /// Root type of the tool input schema. - [JsonPropertyName("type")] - public BuiltinToolInputSchemaType Type { get; set; } + /// Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. `fast` is an integrator-only latency preset, not a first-party GitHub Copilot product 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; } } -/// Rust-owned metadata and input schema for a built-in tool. +/// Installed plugin record for a session, with marketplace, version, install time, enabled state, cache path, and source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolDescriptor +public sealed class SessionInstalledPlugin { - /// Model-facing description of the tool's behavior. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// Optional custom input format used instead of a JSON Schema. - [JsonPropertyName("format")] - public BuiltinToolFormat? Format { get; set; } + /// Path where the plugin is cached locally. + [JsonPropertyName("cache_path")] + public string? CachePath { get; set; } - /// Whether the tool provides a specialized intention summary. - [JsonPropertyName("hasSummariseIntention")] - public bool HasSummariseIntention { get; set; } + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// JSON Schema for the tool input, or null when the tool uses a custom format. - [JsonPropertyName("inputSchema")] - public BuiltinToolInputSchema? InputSchema { get; set; } + /// Installation timestamp (ISO-8601). + [JsonPropertyName("installed_at")] + public string InstalledAt { get; set; } = string.Empty; - /// Optional supplemental usage instructions for the tool. - [JsonPropertyName("instructions")] - public string? Instructions { get; set; } + /// 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; } - /// Whether the tool executes commands in a terminal. - [JsonPropertyName("isTerminal")] - public bool IsTerminal { get; set; } + /// Marketplace the plugin came from (empty string for direct repo installs). + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; - /// Stable name used to invoke the built-in tool. + /// Plugin name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Policy describing which tool metadata may be recorded without obfuscation. - [JsonPropertyName("safeForTelemetry")] - public JsonElement SafeForTelemetry { get; set; } + /// Source descriptor for direct repo installs (when marketplace is empty). + [JsonPropertyName("source")] + public JsonElement? Source { get; set; } - /// Optional human-readable title for the tool. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. + [JsonPropertyName("source_sha")] + public string? SourceSha { get; set; } - /// Optional tool category discriminator. - [JsonPropertyName("type")] - public string? Type { get; set; } + /// Installed version, if known. + [JsonPropertyName("version")] + public string? Version { get; set; } } -/// Rust-owned built-in tool descriptors for the session. +/// Custom model-provider configuration (BYOK). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsGetBuiltinDescriptorsResult +public sealed class ProviderConfig { - /// Built-in tool descriptors materialized for the session. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } -} + /// API key. Optional for local providers like Ollama. + [JsonPropertyName("apiKey")] + public string? ApiKey { get; set; } -/// Shell-specific names and description lines used to materialize built-in shell tool descriptors. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsShellDescriptorConfig -{ - /// Additional model-facing shell description lines. - [JsonPropertyName("descriptionLines")] - public IList DescriptionLines { get => field ??= []; set; } + /// Azure-specific provider options. + [JsonPropertyName("azure")] + public ProviderConfigAzure? Azure { get; set; } - /// Human-readable shell name. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; + /// API endpoint URL. + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = string.Empty; - /// Tool name used to list active shells. - [JsonPropertyName("listShellsToolName")] - public string ListShellsToolName { get; set; } = string.Empty; + /// Bearer token for authentication. Sets the Authorization header directly. Takes precedence over apiKey when both are set. + [JsonPropertyName("bearerToken")] + public string? BearerToken { get; set; } - /// Tool name used to read shell output. - [JsonPropertyName("readShellToolName")] - public string ReadShellToolName { get; set; } = string.Empty; + /// When true, the SDK client supplies bearer tokens on demand: the runtime calls the client-session `providerToken.getToken` callback before each request and applies the returned token as an `Authorization: Bearer <token>` header. This is the bearer/OAuth scheme used by Azure AD / managed-identity tokens and provider OAuth access tokens (including Anthropic's), not a provider-specific API-key header such as Anthropic's `x-api-key`. The token-acquiring function itself stays on the SDK side and is never serialized; only this flag crosses the wire. When set alongside `apiKey`/`bearerToken`, the callback takes precedence: the runtime applies the token returned by `providerToken.getToken` as the `Authorization: Bearer` header for each request and does not send the static credential. + [JsonPropertyName("hasBearerTokenProvider")] + public bool? HasBearerTokenProvider { get; set; } - /// Tool name used to start shell commands. - [JsonPropertyName("shellToolName")] - public string ShellToolName { get; set; } = string.Empty; + /// Custom HTTP headers to include in all outbound requests to the provider. + [JsonPropertyName("headers")] + public IDictionary? Headers { get; set; } - /// Stable shell type identifier. - [JsonPropertyName("shellType")] - public string ShellType { get; set; } = string.Empty; + /// Maximum context window tokens for the model. + [JsonPropertyName("maxContextWindowTokens")] + public double? MaxContextWindowTokens { get; set; } - /// Tool name used to stop shell commands. - [JsonPropertyName("stopShellToolName")] - public string StopShellToolName { get; set; } = string.Empty; -} + /// Maximum output tokens for the model. + [JsonPropertyName("maxOutputTokens")] + public double? MaxOutputTokens { get; set; } -/// Options controlling how Rust-owned built-in tool descriptors are materialized. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsGetBuiltinDescriptorsRequest -{ - /// Whether background task completion notifications are enabled. - [JsonPropertyName("backgroundTaskNotificationsEnabled")] - public bool? BackgroundTaskNotificationsEnabled { get; set; } + /// Maximum prompt/input tokens for the model. + [JsonPropertyName("maxPromptTokens")] + public double? MaxPromptTokens { get; set; } - /// Whether tool descriptors should include authoring metadata. - [JsonPropertyName("includeAuthor")] - public bool? IncludeAuthor { get; set; } + /// Overrides for model capabilities when they cannot be inferred from modelId. + [JsonPropertyName("modelCapabilities")] + public ModelCapabilitiesOverride? ModelCapabilities { get; set; } - /// Whether descriptors should favor fewer user-intervention prompts. - [JsonPropertyName("reduceUserIntervention")] - public bool? ReduceUserIntervention { get; set; } + /// Well-known model ID used for capability lookup. When set, agent behavior config and token limits are inferred from this model. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The product serving the model, reported in telemetry as `model_provider`. Set it when `type` alone cannot identify the product, such as Ollama or LM Studio behind an OpenAI-compatible endpoint. Only affects telemetry. + [JsonPropertyName("modelProvider")] + public ProviderConfigModelProvider? ModelProvider { get; set; } - /// Shell-specific names and description lines for shell tools. - [JsonPropertyName("shellConfig")] - public ToolsShellDescriptorConfig? ShellConfig { get; set; } + /// Provider name used for model and telemetry attribution. + [JsonPropertyName("providerName")] + public string? ProviderName { get; set; } - /// Whether the configured shell supports PowerShell 7 syntax. - [JsonPropertyName("shellSupportsPowerShell7Syntax")] - public bool? ShellSupportsPowerShell7Syntax { get; set; } + /// Provider transport. Defaults to "http". + [JsonPropertyName("transport")] + public ProviderConfigTransport? Transport { get; set; } - /// Default shell timeout in milliseconds. - [JsonPropertyName("shellTimeoutMs")] - public double? ShellTimeoutMs { get; set; } + /// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. + [JsonPropertyName("type")] + public ProviderConfigType? Type { get; set; } - /// Whether semantic skill lookup is available. - [JsonPropertyName("skillEmbeddingEnabled")] - public bool? SkillEmbeddingEnabled { get; set; } + /// Wire API format (openai/azure only). Defaults to "completions". + [JsonPropertyName("wireApi")] + public ProviderConfigWireApi? WireApi { get; set; } + + /// The model identifier sent to the provider API for inference (the "wire" model), as opposed to modelId which is the well-known base. + [JsonPropertyName("wireModel")] + public string? WireModel { get; set; } } -/// Task completion notification with summary from the agent. +/// +/// Command-scoped GitHub credential injection for the shell commands an agent runs. +/// +/// Each channel is opt-in and independent, and injection is scoped to the individual command +/// spawn: the credential is resolved from the session's *current* authentication at every spawn +/// and reaches only spawns whose script actually invokes `git` or `gh`. Because nothing is +/// retained between spawns, replacing the session credential (`session.gitHubAuth.setCredentials`) +/// changes what the next spawned command presents — which seeding a credential into the runtime +/// process's own environment cannot do, since a child's environment is fixed at `exec`. +/// +/// The credential is matched to the host it authenticates to, so a github.com credential is never +/// presented to a GitHub Enterprise host and vice versa. Where a channel cannot express that +/// boundary it injects nothing rather than crossing it -- see `gh` below. +/// +/// This is independent of `sandboxConfig`: it is a decision about which identity the agent +/// presents, not about what the agent may touch, and it works on every platform whether or not +/// an OS sandboxing backend is available. `sandboxConfig.auth` remains the sandbox-scoped +/// spelling and is additive with this one. +/// [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskCompleteData +public sealed class ShellCredentials { - /// Structured blocker details when outcome is blocked. - [JsonPropertyName("blocker")] - public TaskBlocker? Blocker { get; set; } - - /// Active autopilot objective ID evaluated by the completion reviewer. - [JsonPropertyName("objectiveId")] - public long? ObjectiveId { get; set; } - - /// Semantic completion decision. Absent on legacy events and invalid tool calls. - [JsonPropertyName("outcome")] - public TaskCompletionOutcome? Outcome { get; set; } - - /// Label-safe runtime rationale for the completion decision (e.g. a cancellation or pause/resume downgrade), when one applies. Reviewer-authored rationale is intentionally omitted here because this event has no IFC label channel; the reviewer's findings remain available through its own labeled sub-agent events. - [JsonPropertyName("reason")] - public string? Reason { get; set; } - - /// Whether the task was accepted as complete. False when validation failed or completion was rejected or blocked by the reviewer. - [JsonPropertyName("success")] - public bool? Success { get; set; } + /// + /// Whether to authenticate the agent's `gh` commands as the session's GitHub credential, by + /// exporting `GH_TOKEN` to a spawn that runs `gh`. Any inherited `gh` credential is removed from + /// spawns that do not, so the credential stays command-scoped. + /// + /// Applies to a github.com credential only. `gh` picks its credential variable from the host a + /// command targets rather than the one the credential belongs to, and the command can choose that + /// target, so `GH_ENTERPRISE_TOKEN` would offer a single-tenant enterprise credential to every + /// other enterprise host. A session whose credential is enterprise-scoped therefore runs `gh` + /// unauthenticated; its `git` commands are unaffected, because `http.<host>.extraheader` is scoped + /// to one host by construction. Default: false (opt-in). + /// + [JsonPropertyName("gh")] + public bool? Gh { get; set; } - /// Summary of the completed task, provided by the agent. - [JsonPropertyName("summary")] - public string? Summary { get; set; } + /// + /// Whether to authenticate the agent's `git` commands as the session's GitHub credential, by + /// injecting an `http.<host>.extraheader` (plus `insteadOf` rewrites so SSH-spelled remotes for + /// that host use the authenticated HTTPS transport). Applied only to a spawn that runs a + /// remote-contacting `git` subcommand. Default: false (opt-in). + /// + [JsonPropertyName("git")] + public bool? Git { get; set; } } -/// Binary result returned by a tool for the model. +/// A host-provided script sourced before each built-in shell command when its shell target matches the active shell. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm +public sealed class ShellInitScript { - /// Base64-encoded binary data. - [Base64String] - [JsonPropertyName("data")] - public string Data { get; set; } = string.Empty; - - /// Human-readable description of the binary data. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// Optional metadata from the producing tool. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } - - /// MIME type of the binary data. - [JsonPropertyName("mimeType")] - public string MimeType { get; set; } = string.Empty; + /// Path to the script to source. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. - [JsonPropertyName("type")] - public ExternalToolTextResultForLlmBinaryResultsForLlmType Type { get; set; } + /// Built-in shell that may source this script. + [JsonPropertyName("shell")] + public ShellInitScriptShell Shell { get; set; } } -/// A content block within a tool result, which may be text, terminal output, image, audio, or a resource. -/// Polymorphic base type discriminated by type. +/// Per-session settings for built-in shell tools. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentText), "text")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentTerminal), "terminal")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentShellExit), "shell_exit")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentImage), "image")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentAudio), "audio")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentResourceLink), "resource_link")] -[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentResource), "resource")] -public partial class ExternalToolTextResultForLlmContent +public sealed class ShellOptions { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} + /// Command-scoped GitHub credential injection for shell commands. + [JsonPropertyName("credentials")] + public ShellCredentials? Credentials { get; set; } + /// Controls automatic non-interactive profile loading where supported. Explicit initScripts are unaffected. + [JsonPropertyName("initProfile")] + public ShellInitProfile? InitProfile { get; set; } -/// Plain text content block. -/// The text variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentText : ExternalToolTextResultForLlmContent -{ - /// - [JsonIgnore] - public override string Type => "text"; + /// + /// Ordered host-provided script paths sourced before each built-in shell command when the + /// entry's shell target matches the active shell. Use these for rc files, environment setup scripts, + /// or other custom scripts. A script that returns a nonzero status is reported, and later scripts + /// and the user command continue while the shell remains running. Because scripts are sourced into + /// the command shell, `exit`, `exec`, failures under `set -e`, or other shell-terminating behavior + /// can prevent continuation. Script standard output is preserved; Bash script stderr is discarded, + /// PowerShell exception messages are replaced, and runtime-generated failure notices omit + /// configured script paths. When sandboxing is enabled, each script must already be readable under + /// the active sandbox filesystem policy. Pass an empty array to clear the list. + /// + [JsonPropertyName("initScripts")] + public IList? InitScripts { get; set; } - /// The text content. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// + /// Flags passed to the active built-in shell process on startup, replacing its default flags. + /// When omitted, the built-in Bash shell uses `--norc --noprofile`, + /// and the built-in PowerShell shell uses `-NoProfile -NoLogo`. + /// + [JsonPropertyName("processFlags")] + public IList? ProcessFlags { get; set; } } -/// Terminal/shell output content block with optional exit code and working directory. -/// The terminal variant of . +/// Patch of mutable session options to apply to the running session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentTerminal : ExternalToolTextResultForLlmContent +internal sealed class SessionUpdateOptionsParams { - /// - [JsonIgnore] - public override string Type => "terminal"; + /// Additional content-exclusion policies to merge into the session's policy set. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonPropertyName("additionalContentExclusionPolicies")] + public IList? AdditionalContentExclusionPolicies { get; set; } - /// Working directory where the command was executed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// Runtime context discriminator (e.g., `cli`, `actions`). + [JsonPropertyName("agentContext")] + public string? AgentContext { get; set; } - /// Process exit code, if the command has completed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("exitCode")] - public long? ExitCode { get; set; } + /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. + [JsonPropertyName("allowAllMcpServerInstructions")] + public bool? AllowAllMcpServerInstructions { get; set; } - /// Terminal/shell output text. - [JsonPropertyName("text")] - public required string Text { get; set; } -} + /// Whether to disable the `ask_user` tool (encourages autonomous behavior). + [JsonPropertyName("askUserDisabled")] + public bool? AskUserDisabled { get; set; } -/// Shell command exit metadata with optional output preview. -/// The shell_exit variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentShellExit : ExternalToolTextResultForLlmContent -{ - /// - [JsonIgnore] - public override string Type => "shell_exit"; + /// Allowlist of tool names available to this session. + [JsonPropertyName("availableTools")] + public IList? AvailableTools { get; set; } - /// Working directory where the shell command was executed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// Options scoped to the built-in CAPI (Copilot API) provider. + [JsonPropertyName("capi")] + public CapiSessionOptions? Capi { get; set; } - /// Exit code from the completed shell command. - [JsonPropertyName("exitCode")] - public required long ExitCode { get; set; } + /// Identifier of the client driving the session. + [JsonPropertyName("clientName")] + public string? ClientName { 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; } + /// Whether to include the `Co-authored-by` trailer in commit messages. + [JsonPropertyName("coauthorEnabled")] + public bool? CoauthorEnabled { 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")] - public string? OutputPreview { get; set; } + /// Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. + [JsonPropertyName("contextTier")] + public OptionsUpdateContextTier? ContextTier { get; set; } - /// Whether outputPreview is known to be incomplete or truncated. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("outputTruncated")] - public bool? OutputTruncated { get; set; } + /// Whether to allow auto-mode continuation across turns. + [JsonPropertyName("continueOnAutoMode")] + public bool? ContinueOnAutoMode { get; set; } - /// Shell id, as assigned by Copilot runtime. - [JsonPropertyName("shellId")] - public required string ShellId { get; set; } -} + /// Override URL for the Copilot API endpoint. + [JsonPropertyName("copilotUrl")] + public string? CopilotUrl { get; set; } -/// Image content block with base64-encoded data. -/// The image variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentImage : ExternalToolTextResultForLlmContent -{ - /// - [JsonIgnore] - public override string Type => "image"; + /// Whether to default custom agents to local-only execution. + [JsonPropertyName("customAgentsLocalOnly")] + public bool? CustomAgentsLocalOnly { get; set; } - /// Base64-encoded image data. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } + /// Instruction source IDs to exclude from the system prompt. + [JsonPropertyName("disabledInstructionSources")] + public IList? DisabledInstructionSources { get; set; } - /// MIME type of the image (e.g., image/png, image/jpeg). - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } -} + /// Skill IDs that should be excluded from this session. + [JsonPropertyName("disabledSkills")] + public IList? DisabledSkills { get; set; } -/// Audio content block with base64-encoded data. -/// The audio variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentAudio : ExternalToolTextResultForLlmContent -{ - /// - [JsonIgnore] - public override string Type => "audio"; + /// Whether to enable loading of `.github/hooks/` filesystem hooks. Separate from the SDK callback hook mechanism. + [JsonPropertyName("enableFileHooks")] + public bool? EnableFileHooks { get; set; } - /// Base64-encoded audio data. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } + /// Whether to enable host git operations (context resolution, child repo scanning, git info in system prompt). + [JsonPropertyName("enableHostGitOperations")] + public bool? EnableHostGitOperations { get; set; } - /// MIME type of the audio (e.g., audio/wav, audio/mpeg). - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } -} + /// Whether to discover custom instructions on demand after successful file views (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md surfacing). Combined with `skipCustomInstructions`. + [JsonPropertyName("enableOnDemandInstructionDiscovery")] + public bool? EnableOnDemandInstructionDiscovery { get; set; } -/// Icon image for a resource. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExternalToolTextResultForLlmContentResourceLinkIcon -{ - /// MIME type of the icon image. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } + /// Whether to surface reasoning-summary events from the model. + [JsonPropertyName("enableReasoningSummaries")] + public bool? EnableReasoningSummaries { get; set; } - /// Available icon sizes (e.g., ['16x16', '32x32']). - [JsonPropertyName("sizes")] - public IList? Sizes { get; set; } + /// Whether shell-script safety heuristics are enabled. + [JsonPropertyName("enableScriptSafety")] + public bool? EnableScriptSafety { get; set; } - /// URL or path to the icon image. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; + /// Whether to enable cross-session store writes and reads. + [JsonPropertyName("enableSessionStore")] + public bool? EnableSessionStore { get; set; } - /// Theme variant this icon is intended for. - [JsonPropertyName("theme")] - public ExternalToolTextResultForLlmContentResourceLinkIconTheme? Theme { get; set; } -} + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. + [JsonPropertyName("enableSkills")] + public bool? EnableSkills { get; set; } -/// Resource link content block referencing an external resource. -/// The resource_link variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentResourceLink : ExternalToolTextResultForLlmContent -{ - /// - [JsonIgnore] - public override string Type => "resource_link"; + /// Whether to stream model responses. + [JsonPropertyName("enableStreaming")] + public bool? EnableStreaming { get; set; } - /// Human-readable description of the resource. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description { get; set; } + /// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). + [JsonPropertyName("envValueMode")] + public OptionsUpdateEnvValueMode? EnvValueMode { get; set; } - /// Icons associated with this resource. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("icons")] - public IList? Icons { get; set; } + /// Override directory for the session-events log. When unset, the runtime's default events log directory is used. + [JsonPropertyName("eventsLogDirectory")] + public string? EventsLogDirectory { get; set; } - /// MIME type of the resource content. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } + /// Whether subagent callback events should be forwarded into the session event log sink. + [JsonPropertyName("eventsLogIncludesSubagents")] + public bool? EventsLogIncludesSubagents { get; set; } - /// Resource name identifier. - [JsonPropertyName("name")] - public required string Name { get; set; } + /// Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. + [JsonPropertyName("excludedBuiltinAgents")] + public IList? ExcludedBuiltinAgents { get; set; } - /// Size of the resource in bytes. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("size")] - public long? Size { get; set; } + /// Denylist of tool names for this session. + [JsonPropertyName("excludedTools")] + public IList? ExcludedTools { get; set; } - /// Human-readable display title for the resource. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Map of feature-flag IDs to their boolean enabled state. + [JsonPropertyName("featureFlags")] + public IDictionary? FeatureFlags { get; set; } - /// URI identifying the resource. - [JsonPropertyName("uri")] - public required string Uri { get; set; } -} + /// Skill scan directories and descendants excluded from discovery. Supports `~`-relative paths. + [JsonPropertyName("ignoredSkillsLocations")] + public IList? IgnoredSkillsLocations { get; set; } -/// Embedded resource content block with inline text or binary data. -/// The resource variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentResource : ExternalToolTextResultForLlmContent -{ - /// - [JsonIgnore] - public override string Type => "resource"; + /// Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. Set to null to remove the allowlist restriction. + [JsonPropertyName("includedBuiltinAgents")] + public IList? IncludedBuiltinAgents { get; set; } - /// The embedded resource contents, either text or base64-encoded binary. - [JsonPropertyName("resource")] - public required JsonElement Resource { 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; } -/// A message injected by a tool result. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolResultNewMessage -{ - /// Message content to inject after the tool result. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// 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; } - /// Source attributed to the injected message. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; -} + /// Stable integration identifier used for analytics and rate-limit attribution. + [JsonPropertyName("integrationId")] + public string? IntegrationId { get; set; } -/// RPC data type for TaskCompletionDecision operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskCompletionDecision -{ - /// Objective eligibility token captured when the decision was evaluated. - [JsonPropertyName("completionEligibilityToken")] - public long? CompletionEligibilityToken { get; set; } + /// Whether experimental capabilities are enabled. + [JsonPropertyName("isExperimentalMode")] + public bool? IsExperimentalMode { get; set; } - /// Whether completion was accepted after the reviewer-rejection budget was exhausted. - [JsonPropertyName("completionRejectionBudgetExhausted")] - public bool? CompletionRejectionBudgetExhausted { get; set; } + /// Whether interactive shell sessions are logged. + [JsonPropertyName("logInteractiveShells")] + public bool? LogInteractiveShells { get; set; } - /// Active autopilot objective evaluated by the completion reviewer. - [JsonPropertyName("objectiveId")] - public long? ObjectiveId { get; set; } + /// Identifier sent to LSP-style integrations. + [JsonPropertyName("lspClientName")] + public string? LspClientName { get; set; } - /// Semantic result of evaluating the task completion request. - [JsonPropertyName("outcome")] - public TaskCompletionOutcome Outcome { get; set; } + /// Whether to expose the `manage_schedule` tool to the agent. The runtime always owns the per-session schedule registry; this flag only controls tool exposure (typically gated to staff users). + [JsonPropertyName("manageScheduleEnabled")] + public bool? ManageScheduleEnabled { get; set; } - /// Rationale for the completion decision, when one is available. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// 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. + [JsonPropertyName("maxInlineBinaryBytes")] + public long? MaxInlineBinaryBytes { get; set; } - /// Whether the rationale was derived from completion-reviewer output. - [JsonPropertyName("reviewerDerived")] - public bool? ReviewerDerived { get; set; } + /// The model ID to use for assistant turns. + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Information-flow metadata captured from the completion reviewer. - [JsonPropertyName("reviewerResultMeta")] - public JsonElement? ReviewerResultMeta { get; set; } -} + /// Per-property model capability overrides for the selected model. + [JsonPropertyName("modelCapabilitiesOverrides")] + public ModelCapabilitiesOverride? ModelCapabilitiesOverrides { get; set; } -/// Expanded canonical result returned by a session tool. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolResultExpanded -{ - /// Base64-encoded binary results returned to the model. - [JsonPropertyName("binaryResultsForLlm")] - public IList? BinaryResultsForLlm { get; set; } + /// Organization-level custom instructions to inject into the system prompt. + [JsonPropertyName("organizationCustomInstructions")] + public string? OrganizationCustomInstructions { get; set; } - /// Sources returned by the tool that the model may cite. - [JsonPropertyName("citableSources")] - public IList? CitableSources { get; set; } + /// Custom model-provider configuration (BYOK). + [JsonPropertyName("provider")] + public ProviderConfig? Provider { get; set; } - /// Structured content blocks returned to the model. - [JsonPropertyName("contents")] - public IList? Contents { get; set; } + /// Reasoning effort for the selected model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. When omitted, no effort override is applied. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } - /// Error message for an unsuccessful execution. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Reasoning summary mode for supported model clients. + [JsonPropertyName("reasoningSummary")] + public OptionsUpdateReasoningSummary? ReasoningSummary { get; set; } - /// Metadata propagated with the tool result, including information-flow labels. - [JsonPropertyName("mcpMeta")] - public IDictionary? McpMeta { get; set; } + /// Whether the session is running in an interactive UI. + [JsonPropertyName("runningInInteractiveMode")] + public bool? RunningInInteractiveMode { get; set; } - /// Messages to inject after the tool result. - [JsonPropertyName("newMessages")] - public IList? NewMessages { get; set; } + /// Resolved sandbox configuration. + [JsonPropertyName("sandboxConfig")] + public SandboxConfig? SandboxConfig { get; set; } - /// Whether post-tool-use failure hooks have already processed this result. - [JsonPropertyName("postToolUseFailureHooksProcessed")] - public bool? PostToolUseFailureHooksProcessed { get; set; } + /// Origin of the sandbox choice. Settings-derived origins (never_configured, user_enabled, user_disabled, repository_policy) let managed policy floor a host preference; explicit below-floor changes remain policy conflicts unless a session opt-out is authorized. Also used for telemetry provenance. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonPropertyName("sandboxConfigSource")] + public SandboxConfigSource? SandboxConfigSource { get; set; } - /// Execution outcome classification. - [JsonPropertyName("resultType")] - public ToolResultType ResultType { 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; } - /// Detailed log content available for session display. - [JsonPropertyName("sessionLog")] - public string? SessionLog { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Skill invocation metadata produced by the tool. - [JsonPropertyName("skillInvocation")] - public JsonElement? SkillInvocation { get; set; } + /// Optional session limits. Pass null to clear the session limits. + [JsonPropertyName("sessionLimits")] + public SessionLimitsConfig? SessionLimits { get; set; } - /// Whether large-output post-processing should be skipped. - [JsonPropertyName("skipLargeOutputProcessing")] - public bool? SkipLargeOutputProcessing { get; set; } + /// Per-session settings for built-in shell tools. + [JsonPropertyName("shell")] + public ShellOptions? Shell { get; set; } - /// Structured result content in addition to the model-facing text. - [JsonPropertyName("structuredContent")] - public JsonElement? StructuredContent { get; set; } + /// Use shell.initProfile instead. Shell init profile (`None` or `NonInteractive`). + [EditorBrowsable(EditorBrowsableState.Never)] +#if NET5_0_OR_GREATER + [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] +#endif + [JsonPropertyName("shellInitProfile")] + public string? ShellInitProfile { get; set; } - /// Completion-review decision produced by the task-completion tool. - [JsonPropertyName("taskCompletionDecision")] - public TaskCompletionDecision? TaskCompletionDecision { get; set; } + /// PowerShell process flags applied to built-in and user-requested shell commands. + [JsonPropertyName("shellProcessFlags")] + public IList? ShellProcessFlags { get; set; } - /// Text result returned to the model. - [JsonPropertyName("textResultForLlm")] - public string TextResultForLlm { get; set; } = string.Empty; + /// Additional directories to search for skills. + [JsonPropertyName("skillDirectories")] + public IList? SkillDirectories { get; set; } - /// Deferred tool names made available by this result. - [JsonPropertyName("toolReferences")] - public IList? ToolReferences { get; set; } + /// Whether to skip loading custom instruction sources. + [JsonPropertyName("skipCustomInstructions")] + public bool? SkipCustomInstructions { get; set; } - /// Tool-specific telemetry payload. - [JsonPropertyName("toolTelemetry")] - public JsonElement? ToolTelemetry { get; set; } + /// Whether to skip embedding retrieval pipeline initialization and execution. + [JsonPropertyName("skipEmbeddingRetrieval")] + public bool? SkipEmbeddingRetrieval { get; set; } - /// Optional UI resource produced by the tool. - [JsonPropertyName("uiResource")] - public JsonElement? UiResource { get; set; } + /// When true, the selected custom agent's prompt is not injected into the user message (skill context is still injected). Used by automation triggers where the agent prompt is already in the problem statement. + [JsonPropertyName("suppressCustomAgentPrompt")] + public bool? SuppressCustomAgentPrompt { get; set; } + + /// Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. + [JsonPropertyName("toolFilterPrecedence")] + public OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence { get; set; } + + /// Optional path for trajectory output. + [JsonPropertyName("trajectoryFile")] + public string? TrajectoryFile { get; set; } + + /// Output verbosity level for supported models. + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } + + /// Absolute working-directory path for shell tools. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// Task-completion tool arguments and final result used to build a label-safe session event payload. +/// Parameters for (re)loading the merged LSP configuration set. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsTaskCompleteEventDataRequest +internal sealed class LspInitializeRequest { - /// Final expanded result returned by the task_complete tool. - [JsonPropertyName("finalResult")] - public ToolResultExpanded FinalResult { get => field ??= new(); set; } + /// Force re-initialization even when LSP configs were already loaded for the working directory. + [JsonPropertyName("force")] + public bool? Force { get; set; } + + /// Git root used as the boundary when traversing for project-level LSP configs (supports monorepos). + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Arguments supplied to the completed task_complete tool call. - [JsonPropertyName("toolArgs")] - public JsonElement ToolArgs { get; set; } + /// Working directory used to load project-level LSP configs. Defaults to the session working directory when omitted. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// Indicates whether the external tool call result was handled successfully. +/// Discovered extension metadata, including source-qualified ID, name, discovery source, status, and optional process ID. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HandlePendingToolCallResult +public sealed class Extension { - /// Whether the tool call result was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper', 'plugin:my-plugin:my-ext'). + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; -/// Pending external tool call request ID, with the tool result or an error describing why it failed. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HandlePendingToolCallRequest -{ - /// Error message if the tool call failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Extension name (directory name). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Request ID of the pending tool call. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Process ID if the extension is running. + [JsonPropertyName("pid")] + public long? Pid { get; set; } - /// Tool call result (string or expanded result object). - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Discovery source: project (.github/extensions/), user (~/.copilot/extensions/), plugin (installed plugin), or session (session-state/<id>/extensions/). + [JsonPropertyName("source")] + public ExtensionSource Source { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Current status: running, disabled, failed, or starting. + [JsonPropertyName("status")] + public ExtensionStatus Status { get; set; } } -/// Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. +/// Extensions discovered for the session, with their current status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsInitializeAndValidateResult +public sealed class ExtensionList { + /// Discovered extensions and their current status. + [JsonPropertyName("extensions")] + public IList Extensions { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionToolsInitializeAndValidateRequest +internal sealed class SessionExtensionsListRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Lightweight metadata for a currently initialized session tool. +/// Source-qualified extension identifier to enable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CurrentToolMetadata +internal sealed class ExtensionsEnableRequest { - /// Whether the tool is loaded on demand via tool search. - [JsonPropertyName("deferLoading")] - public bool? DeferLoading { get; set; } - - /// Tool description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Source-qualified extension ID to enable. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// JSON Schema for tool input. - [JsonPropertyName("input_schema")] - public IDictionary? InputSchema { get; set; } - - /// MCP server name for MCP-backed tools. - [JsonPropertyName("mcpServerName")] - public string? McpServerName { get; set; } - - /// Raw MCP tool name for MCP-backed tools. - [JsonPropertyName("mcpToolName")] - public string? McpToolName { get; set; } - - /// Model-facing tool name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Optional MCP/config namespaced tool name. - [JsonPropertyName("namespacedName")] - public string? NamespacedName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Current lightweight tool metadata snapshot for the session. +/// Source-qualified extension identifier to disable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsGetCurrentMetadataResult +internal sealed class ExtensionsDisableRequest { - /// Current tool metadata, or null when tools have not been initialized yet. - [JsonPropertyName("tools")] - public IList? Tools { get; set; } + /// Source-qualified extension ID to disable. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionToolsGetCurrentMetadataRequest +internal sealed class SessionExtensionsReloadRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Empty result after replacing the calling connection's externally implemented tools. +/// Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsSetResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(PushAttachmentFile), "file")] +[JsonDerivedType(typeof(PushAttachmentDirectory), "directory")] +[JsonDerivedType(typeof(PushAttachmentSelection), "selection")] +[JsonDerivedType(typeof(PushAttachmentGitHubReference), "github_reference")] +[JsonDerivedType(typeof(PushAttachmentGitHubCommit), "github_commit")] +[JsonDerivedType(typeof(PushAttachmentGitHubRelease), "github_release")] +[JsonDerivedType(typeof(PushAttachmentGitHubActionsJob), "github_actions_job")] +[JsonDerivedType(typeof(PushAttachmentGitHubRepository), "github_repository")] +[JsonDerivedType(typeof(PushAttachmentGitHubFileDiff), "github_file_diff")] +[JsonDerivedType(typeof(PushAttachmentGitHubTreeComparison), "github_tree_comparison")] +[JsonDerivedType(typeof(PushAttachmentGitHubUrl), "github_url")] +[JsonDerivedType(typeof(PushAttachmentGitHubFile), "github_file")] +[JsonDerivedType(typeof(PushAttachmentGitHubSnippet), "github_snippet")] +[JsonDerivedType(typeof(PushAttachmentBlob), "blob")] +[JsonDerivedType(typeof(PushAttachmentExtensionContext), "extension_context")] +public partial class PushAttachment { + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Serializable definition of a caller-implemented tool whose execution is handled over the SDK connection. + +/// Optional line range to scope the attachment to a specific section of the file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolExternalToolDefinition +public sealed class PushAttachmentFileLineRange { - /// Tool-loading deferral policy. - [JsonPropertyName("defer")] - public ProtocolExternalToolDefer? Defer { get; set; } + /// End line number (1-based, inclusive). + [JsonPropertyName("end")] + public long End { get; set; } - /// Model-visible explanation of what the tool does. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Start line number (1-based). + [JsonPropertyName("start")] + public long Start { get; set; } +} - /// Whether the tool executes commands in a terminal. - [JsonPropertyName("isTerminal")] - public bool? IsTerminal { get; set; } +/// File attachment. +/// The file variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentFile : PushAttachment +{ + /// + [JsonIgnore] + public override string Type => "file"; - /// Optional caller-defined metadata associated with the tool. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } + /// User-facing display name for the attachment. + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } - /// Unique model-visible tool name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Optional line range to scope the attachment to a specific section of the file. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lineRange")] + public PushAttachmentFileLineRange? LineRange { get; set; } - /// Whether this definition replaces a built-in tool with the same name. - [JsonPropertyName("overridesBuiltInTool")] - public bool? OverridesBuiltInTool { get; set; } + /// Absolute file path. + [JsonPropertyName("path")] + public required string Path { get; set; } +} - /// JSON Schema describing the tool's input arguments. - [JsonPropertyName("parameters")] - public IDictionary? Parameters { get; set; } +/// Directory attachment. +/// The directory variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentDirectory : PushAttachment +{ + /// + [JsonIgnore] + public override string Type => "directory"; - /// Whether execution bypasses the normal tool permission prompt. - [JsonPropertyName("skipPermission")] - public bool? SkipPermission { get; set; } + /// User-facing display name for the attachment. + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Absolute directory path. + [JsonPropertyName("path")] + public required string Path { get; set; } } -/// Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. +/// End position of the selection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsSetRequest +public sealed class PushAttachmentSelectionDetailsEnd { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// End character offset within the line (0-based). + [JsonPropertyName("character")] + public long Character { get; set; } - /// Complete replacement list for the calling connection. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } + /// End line number (0-based). + [JsonPropertyName("line")] + public long Line { get; set; } } -/// Empty result after applying subagent settings. +/// Start position of the selection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsUpdateSubagentSettingsResult +public sealed class PushAttachmentSelectionDetailsStart { + /// Start character offset within the line (0-based). + [JsonPropertyName("character")] + public long Character { get; set; } + + /// Start line number (0-based). + [JsonPropertyName("line")] + public long Line { get; set; } } -/// Subagent model, reasoning effort, context tier, and auto-invocation settings. +/// Position range of the selection within the file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SubagentSettingsEntry +public sealed class PushAttachmentSelectionDetails { - /// Whether this agent's runtime-defined proactive invocation prompting is enabled, if supported. Currently consumed by the built-in rubber-duck agent. - [JsonPropertyName("autoInvoke")] - public bool? AutoInvoke { get; set; } - - /// Context tier override for matching subagents. - [JsonPropertyName("contextTier")] - public SubagentSettingsEntryContextTier? ContextTier { get; set; } - - /// Reasoning effort override for matching subagents. - [JsonPropertyName("effortLevel")] - public string? EffortLevel { get; set; } - - /// Model override for matching subagents. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// End position of the selection. + [JsonPropertyName("end")] + public PushAttachmentSelectionDetailsEnd End { get => field ??= new(); set; } - /// Whether the configured model strategy is preferred or required. - [JsonPropertyName("modelPolicy")] - public AgentModelPolicy? ModelPolicy { get; set; } + /// Start position of the selection. + [JsonPropertyName("start")] + public PushAttachmentSelectionDetailsStart Start { get => field ??= new(); set; } } -/// Configured per-agent subagent overrides. -public sealed class UpdateSubagentSettingsRequestSubagents +/// Code selection attachment from an editor. +/// The selection variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentSelection : PushAttachment { - /// Per-agent settings keyed by subagent agent_type. - [JsonPropertyName("agents")] - public IDictionary? Agents { get; set; } + /// + [JsonIgnore] + public override string Type => "selection"; - /// Names of subagents the user has turned off; they cannot be dispatched. - [JsonPropertyName("disabledSubagents")] - public IList? DisabledSubagents { get; set; } + /// User-facing display name for the selection. + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } - /// Maximum number of subagents that can run concurrently; applies to usage-based billing users only. - [JsonPropertyName("maxConcurrency")] - public int? MaxConcurrency { get; set; } + /// Absolute path to the file containing the selection. + [JsonPropertyName("filePath")] + public required string FilePath { get; set; } - /// Maximum subagent nesting depth; applies to usage-based billing users only. - [JsonPropertyName("maxDepth")] - public int? MaxDepth { get; set; } + /// Position range of the selection within the file. + [JsonPropertyName("selection")] + public required PushAttachmentSelectionDetails Selection { get; set; } + + /// The selected text content. + [JsonPropertyName("text")] + public required string Text { get; set; } } -/// Subagent settings to apply to the current session. +/// GitHub issue, pull request, or discussion reference. +/// The github_reference variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UpdateSubagentSettingsRequest +public partial class PushAttachmentGitHubReference : PushAttachment { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "github_reference"; - /// Subagent settings to apply, or null to clear the live session override. - [JsonPropertyName("subagents")] - public UpdateSubagentSettingsRequestSubagents? Subagents { get; set; } + /// Issue, pull request, or discussion number. + [JsonPropertyName("number")] + public required long Number { get; set; } + + /// Type of GitHub reference. + [JsonPropertyName("referenceType")] + public required PushAttachmentGitHubReferenceType ReferenceType { get; set; } + + /// Current state of the referenced item (e.g., open, closed, merged). + [JsonPropertyName("state")] + public required string State { get; set; } + + /// Title of the referenced item. + [JsonPropertyName("title")] + public required string Title { get; set; } + + /// URL to the referenced item on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// RPC data type for SessionCommandsList operations. +/// Pointer to a GitHub repository. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionCommandsListRequest +public sealed class PushGitHubRepoRef { - /// Include runtime built-in commands. - [JsonPropertyName("includeBuiltins")] - public bool? IncludeBuiltins { get; set; } + /// Numeric GitHub repository id. + [JsonPropertyName("id")] + public long? Id { get; set; } - /// Include commands registered by protocol clients, including SDK clients and extensions. - [JsonPropertyName("includeClientCommands")] - public bool? IncludeClientCommands { get; set; } + /// Repository name (without owner). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Include enabled user-invocable skills and commands. - [JsonPropertyName("includeSkills")] - public bool? IncludeSkills { get; set; } + /// Repository owner login (user or organization). + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; } -/// RPC data type for SessionCommandsListRequestWithSession operations. +/// Pointer to a GitHub commit. +/// The github_commit variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCommandsListRequestWithSession +public partial class PushAttachmentGitHubCommit : PushAttachment { - /// Include runtime built-in commands. - [JsonPropertyName("includeBuiltins")] - public bool? IncludeBuiltins { get; set; } + /// + [JsonIgnore] + public override string Type => "github_commit"; - /// Include commands registered by protocol clients, including SDK clients and extensions. - [JsonPropertyName("includeClientCommands")] - public bool? IncludeClientCommands { get; set; } + /// First line of the commit message. + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Include enabled user-invocable skills and commands. - [JsonPropertyName("includeSkills")] - public bool? IncludeSkills { get; set; } + /// Full commit SHA. + [JsonPropertyName("oid")] + public required string Oid { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Repository the commit belongs to. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } -/// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(SlashCommandInvocationResultText), "text")] -[JsonDerivedType(typeof(SlashCommandInvocationResultAgentPrompt), "agent-prompt")] -[JsonDerivedType(typeof(SlashCommandInvocationResultCompleted), "completed")] -[JsonDerivedType(typeof(SlashCommandInvocationResultSelectSubcommand), "select-subcommand")] -[JsonDerivedType(typeof(SlashCommandInvocationResultAddTimelineEntry), "add-timeline-entry")] -[JsonDerivedType(typeof(SlashCommandInvocationResultShowDialog), "show-dialog")] -[JsonDerivedType(typeof(SlashCommandInvocationResultSetModel), "set-model")] -[JsonDerivedType(typeof(SlashCommandInvocationResultSetPlanModel), "set-plan-model")] -public partial class SlashCommandInvocationResult -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// URL to the commit on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } } - -/// Slash-command invocation result containing text output plus Markdown/ANSI rendering flags. -/// The text variant of . +/// Pointer to a GitHub release. +/// The github_release variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultText : SlashCommandInvocationResult +public partial class PushAttachmentGitHubRelease : PushAttachment { /// [JsonIgnore] - public override string Kind => "text"; - - /// Whether text contains Markdown. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("markdown")] - public bool? Markdown { get; set; } + public override string Type => "github_release"; - /// Whether ANSI sequences should be preserved. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("preserveAnsi")] - public bool? PreserveAnsi { get; set; } + /// Human-readable release name. + [JsonPropertyName("name")] + public required string Name { get; set; } - /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Repository the release belongs to. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } - /// Present when the invocation changed the sandbox for this session only. Nothing was persisted, so consumers must mirror the change onto the live session rather than reloading settings, and must not treat it as a settings change. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("sandboxSessionChange")] - public SandboxSessionChange? SandboxSessionChange { get; set; } + /// Git tag the release is anchored to. + [JsonPropertyName("tagName")] + public required string TagName { get; set; } - /// Text output for the client to render. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// URL to the release on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. -/// The agent-prompt variant of . +/// Pointer to a GitHub Actions job. +/// The github_actions_job variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultAgentPrompt : SlashCommandInvocationResult +public partial class PushAttachmentGitHubActionsJob : PushAttachment { /// [JsonIgnore] - public override string Kind => "agent-prompt"; - - /// Prompt text to display to the user. - [JsonPropertyName("displayPrompt")] - public required string DisplayPrompt { get; set; } + public override string Type => "github_actions_job"; - /// Optional target session mode for the agent prompt. + /// Terminal conclusion of the job when finished (e.g., success, failure, cancelled). Absent for in-progress jobs. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("mode")] - public SessionMode? Mode { get; set; } + [JsonPropertyName("conclusion")] + public string? Conclusion { get; set; } - /// Optional user-facing notice to show before the prompt is submitted. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("notice")] - public string? Notice { get; set; } + /// Job id within the workflow run. + [JsonPropertyName("jobId")] + public required long JobId { get; set; } - /// Prompt to submit to the agent. - [JsonPropertyName("prompt")] - public required string Prompt { get; set; } + /// Display name of the job. + [JsonPropertyName("jobName")] + public required string JobName { get; set; } - /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Repository the workflow run belongs to. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } + + /// URL to the job on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } + + /// Display name of the workflow the job ran in. + [JsonPropertyName("workflowName")] + public required string WorkflowName { get; set; } } -/// Slash-command invocation result indicating completion, with optional message and settings-change flag. -/// The completed variant of . +/// Pointer to a GitHub repository. +/// The github_repository variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultCompleted : SlashCommandInvocationResult +public partial class PushAttachmentGitHubRepository : PushAttachment { /// [JsonIgnore] - public override string Kind => "completed"; + public override string Type => "github_repository"; - /// Optional user-facing message describing the completed command. + /// Short description of the repository. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Optional target session mode applied without submitting an agent prompt. + /// Git ref this attachment is anchored at (branch, tag, or commit). When absent the default branch is implied. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("mode")] - public SessionMode? Mode { get; set; } + [JsonPropertyName("ref")] + public string? Ref { get; set; } - /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Repository pointer. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } + + /// URL to the repository on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// Selectable slash-command subcommand option with name, description, and optional group label. +/// One side of a file diff (head or base). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandSelectSubcommandOption +public sealed class PushAttachmentGitHubFileDiffSide { - /// Human-readable description of the subcommand. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Repository-relative path to the file. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Optional group label for organizing options. - [JsonPropertyName("group")] - public string? Group { get; set; } + /// Git ref (branch, tag, or commit SHA) the file is read at. + [JsonPropertyName("ref")] + public string Ref { get; set; } = string.Empty; - /// Subcommand name to invoke. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Repository the file lives in. + [JsonPropertyName("repo")] + public PushGitHubRepoRef Repo { get => field ??= new(); set; } } -/// Slash-command invocation result asking the client to present subcommand options for a parent command. -/// The select-subcommand variant of . +/// Pointer to a single-file diff. At least one of `head` and `base` must be present. +/// The github_file_diff variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSelectSubcommand : SlashCommandInvocationResult +public partial class PushAttachmentGitHubFileDiff : PushAttachment { /// [JsonIgnore] - public override string Kind => "select-subcommand"; - - /// Parent command name that requires subcommand selection. - [JsonPropertyName("command")] - public required string Command { get; set; } + public override string Type => "github_file_diff"; - /// Available subcommand options for the client to present. - [JsonPropertyName("options")] - public required IList Options { get; set; } + /// File location on the base side of the diff. Absent for additions. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("base")] + public PushAttachmentGitHubFileDiffSide? Base { get; set; } - /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + /// File location on the head side of the diff. Absent for deletions. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + [JsonPropertyName("head")] + public PushAttachmentGitHubFileDiffSide? Head { get; set; } - /// Human-readable title for the selection UI. - [JsonPropertyName("title")] - public required string Title { get; set; } + /// URL to the diff on GitHub (e.g., a commit, compare, or PR-file URL). + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// RPC data type for SlashCommandTimelineEntry operations. +/// One side of a tree comparison (head or base). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandTimelineEntry +public sealed class PushAttachmentGitHubTreeComparisonSide { - /// What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. - [JsonPropertyName("remediation")] - public RemediationAction? Remediation { get; set; } + /// Repository the revision belongs to. + [JsonPropertyName("repo")] + public PushGitHubRepoRef Repo { get => field ??= new(); set; } - /// Text displayed for the timeline entry. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; - - /// Timeline entry presentation type. - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; - - /// Optional URL associated with the timeline entry. - [JsonPropertyName("url")] - public string? Url { get; set; } + /// Git revision (branch, tag, or commit SHA). + [JsonPropertyName("revision")] + public string Revision { get; set; } = string.Empty; } -/// The add-timeline-entry variant of . +/// Pointer to a comparison between two git revisions. +/// The github_tree_comparison variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultAddTimelineEntry : SlashCommandInvocationResult +public partial class PushAttachmentGitHubTreeComparison : PushAttachment { /// [JsonIgnore] - public override string Kind => "add-timeline-entry"; + public override string Type => "github_tree_comparison"; - /// Timeline entry the host should append. - [JsonPropertyName("entry")] - public required SlashCommandTimelineEntry Entry { get; set; } + /// Base side of the comparison. + [JsonPropertyName("base")] + public required PushAttachmentGitHubTreeComparisonSide Base { get; set; } - /// Optional text the host should prefill into the input editor. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("prefillInput")] - public string? PrefillInput { get; set; } + /// Head side of the comparison. + [JsonPropertyName("head")] + public required PushAttachmentGitHubTreeComparisonSide Head { get; set; } - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// URL to the comparison on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// RPC data type for SlashCommandModelPickerDialog operations. +/// Generic GitHub URL reference. +/// The github_url variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandModelPickerDialog +public partial class PushAttachmentGitHubUrl : PushAttachment { - /// Discriminator for a model-picker dialog. - [JsonPropertyName("kind")] - public string Kind { get; set; } = string.Empty; - - /// Model that should be enabled before it can be selected. - [JsonPropertyName("modelToEnable")] - public string? ModelToEnable { get; set; } - - /// Settings scope the picker should modify. - [JsonPropertyName("scope")] - public string? Scope { get; set; } + /// + [JsonIgnore] + public override string Type => "github_url"; - /// Model-selection target represented by the picker. - [JsonPropertyName("target")] - public string? Target { get; set; } + /// URL to the GitHub resource. + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// The show-dialog variant of . +/// Pointer to a file in a GitHub repository at a specific ref. +/// The github_file variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultShowDialog : SlashCommandInvocationResult +public partial class PushAttachmentGitHubFile : PushAttachment { /// [JsonIgnore] - public override string Kind => "show-dialog"; + public override string Type => "github_file"; - /// Dialog the host should display. - [JsonPropertyName("dialog")] - public required SlashCommandModelPickerDialog Dialog { get; set; } + /// Repository-relative path to the file. + [JsonPropertyName("path")] + public required string Path { get; set; } - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } -} + /// Git ref the file is read at (branch, tag, or commit SHA). + [JsonPropertyName("ref")] + public required string Ref { get; set; } -/// User-settings snapshot to restore if the host cancels the model switch. -public sealed class SlashCommandInvocationResultSetModelRevertOnCancel -{ + /// Repository the file lives in. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } + + /// URL to the file on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// The set-model variant of . +/// Pointer to a line range inside a file in a GitHub repository. +/// The github_snippet variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSetModel : SlashCommandInvocationResult +public partial class PushAttachmentGitHubSnippet : PushAttachment { /// [JsonIgnore] - public override string Kind => "set-model"; - - /// Auto routing profile selected by the command, when the model is Auto. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("autoTier")] - public AutoTier? AutoTier { get; set; } - - /// Model selected by the command. - [JsonPropertyName("model")] - public required string Model { get; set; } - - /// Reasoning effort selected for the model. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + public override string Type => "github_snippet"; - /// Repository settings scope modified by the command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("repoScope")] - public string? RepoScope { get; set; } + /// Line range the snippet covers. + [JsonPropertyName("lineRange")] + public required PushAttachmentFileLineRange LineRange { get; set; } - /// User-settings snapshot to restore if the host cancels the model switch. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("revertOnCancel")] - public SlashCommandInvocationResultSetModelRevertOnCancel? RevertOnCancel { get; set; } + /// Repository-relative path to the file. + [JsonPropertyName("path")] + public required string Path { get; set; } - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Git ref the file is read at (branch, tag, or commit SHA). + [JsonPropertyName("ref")] + public required string Ref { get; set; } - /// Settings scope modified by the command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("scope")] - public string? Scope { get; set; } + /// Repository the file lives in. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } - /// User-facing warning produced while selecting the model. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("warning")] - public string? Warning { get; set; } + /// URL to the snippet on GitHub (with line anchor). + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// The set-plan-model variant of . +/// Blob attachment with inline base64-encoded data. +/// The blob variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSetPlanModel : SlashCommandInvocationResult +public partial class PushAttachmentBlob : PushAttachment { /// [JsonIgnore] - public override string Kind => "set-plan-model"; + public override string Type => "blob"; - /// User-facing confirmation message for the plan-model selection. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Base64-encoded content. + [Base64String] + [JsonPropertyName("data")] + public required string Data { get; set; } - /// Dedicated model selected for plan mode. + /// User-facing display name for the attachment. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("planModel")] - public string? PlanModel { get; set; } + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// MIME type of the inline data. + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } } -/// Slash command name and optional raw input string to invoke. +/// Slim input shape for extension_context attachments; identity fields are runtime-derived. +/// The extension_context variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsInvokeRequest +public partial class PushAttachmentExtensionContext : PushAttachment { - /// Raw input after the command name. - [JsonPropertyName("input")] - public string? Input { get; set; } - - /// Command name. Leading slashes are stripped and the name is matched case-insensitively. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "extension_context"; - /// Optional client surface that initiated the invocation. - [JsonPropertyName("origin")] - public CommandsInvocationOrigin? Origin { get; set; } + /// Caller-supplied JSON payload (required, may be null but not undefined). + [JsonPropertyName("payload")] + public required JsonElement Payload { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Human-readable composer pill label. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("title")] + public required string Title { get; set; } } -/// Whether finalizing the invocation effect succeeded, and the failure reason when it did not. +/// Parameters for session.extensions.sendAttachmentsToMessage. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsFinalizeInvocationEffectResult +internal sealed class SendAttachmentsToMessageParams { - /// Failure reason when the invocation effect could not be finalized. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Attachments to push into the next user-message turn. extension_context entries take the slim shape; standard variants take their full AttachmentSchema shape. + [JsonPropertyName("attachments")] + public IList Attachments { get => field ??= []; set; } - /// Whether the pending invocation effect was finalized successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Optional canvas instance binding the push for provenance. When supplied, the runtime resolves the canvas, verifies it is owned by the calling extension, and stamps canvasId/instanceId onto each extension_context entry. When omitted, no resolution runs and those fields stay unset on the attachment. + [JsonPropertyName("instanceId")] + public string? InstanceId { get; set; } -/// The slash-command result object that produced the pending effect, echoed back unchanged. -public sealed class CommandsFinalizeInvocationEffectRequestEffect -{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. +/// A tool name and arguments to execute through the session's native invocation pipeline. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsFinalizeInvocationEffectRequest +internal sealed class ToolsExecuteRequest { - /// The slash-command result object that produced the pending effect, echoed back unchanged. - [JsonPropertyName("effect")] - public CommandsFinalizeInvocationEffectRequestEffect Effect { get => field ??= new(); set; } + /// Arguments supplied to the tool. + [JsonPropertyName("arguments")] + public JsonElement Arguments { get; set; } - /// Whether the host applied or cancelled the pending invocation effect. - [JsonPropertyName("outcome")] - public CommandsInvocationEffectOutcome Outcome { get; set; } + /// Name of the currently offered tool to execute. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; -} -/// Indicates whether the pending client-handled command was completed successfully. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CommandsHandlePendingCommandResult -{ - /// Whether the command was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Optional identifier used to correlate this invocation with its tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } } -/// Pending command request ID and an optional error if the client handler failed. +/// Custom grammar input format accepted by a built-in tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsHandlePendingCommandRequest +public sealed class BuiltinToolFormat { - /// Error message if the command handler failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Grammar definition accepted by the tool. + [JsonPropertyName("definition")] + public string Definition { get; set; } = string.Empty; - /// Request ID from the command invocation event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Grammar syntax used by the format definition. + [JsonPropertyName("syntax")] + public string Syntax { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Custom input-format discriminator. + [JsonPropertyName("type")] + public BuiltinToolFormatType Type { get; set; } } -/// Error message produced while executing the command, if any. +/// JSON Schema object accepted by a built-in tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExecuteCommandResult +public sealed class BuiltinToolInputSchema { - /// Error message produced while executing the command, if any. Omitted when the handler succeeded. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Root type of the tool input schema. + [JsonPropertyName("type")] + public BuiltinToolInputSchemaType Type { get; set; } } -/// Slash command name and argument string to execute synchronously. +/// Rust-owned metadata and input schema for a built-in tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ExecuteCommandParams +public sealed class BuiltinToolDescriptor { - /// Argument string to pass to the command (empty string if none). - [JsonPropertyName("args")] - public string Args { get; set; } = string.Empty; + /// Model-facing description of the tool's behavior. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Name of the slash command to invoke (without the leading '/'). - [JsonPropertyName("commandName")] - public string CommandName { get; set; } = string.Empty; + /// Optional custom input format used instead of a JSON Schema. + [JsonPropertyName("format")] + public BuiltinToolFormat? Format { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether the tool provides a specialized intention summary. + [JsonPropertyName("hasSummariseIntention")] + public bool HasSummariseIntention { get; set; } -/// Indicates whether the command was accepted into the local execution queue. -/// Data type discriminated by queued. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EnqueueCommandResult -{ - /// The boolean discriminator. - [JsonPropertyName("queued")] - public bool Queued { get; set; } + /// JSON Schema for the tool input, or null when the tool uses a custom format. + [JsonPropertyName("inputSchema")] + public BuiltinToolInputSchema? InputSchema { get; set; } - /// Stable opaque ID of the queued command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("queueId")] - public string? QueueId { get; set; } -} + /// Optional supplemental usage instructions for the tool. + [JsonPropertyName("instructions")] + public string? Instructions { get; set; } -/// Slash-prefixed command string to enqueue for FIFO processing. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EnqueueCommandParams -{ - /// 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. - [JsonPropertyName("command")] - public string Command { get; set; } = string.Empty; + /// Whether the tool executes commands in a terminal. + [JsonPropertyName("isTerminal")] + public bool IsTerminal { get; set; } - /// Optional user-facing text for the queue row. The command string is shown when omitted. - [JsonPropertyName("displayText")] - public string? DisplayText { get; set; } + /// Stable name used to invoke the built-in tool. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Policy describing which tool metadata may be recorded without obfuscation. + [JsonPropertyName("safeForTelemetry")] + public JsonElement SafeForTelemetry { get; set; } -/// Indicates whether the queued-command response was matched to a pending request. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CommandsRespondToQueuedCommandResult -{ - /// Whether a pending queued command with the given request ID was found and resolved. False when the request was already resolved, cancelled, or unknown. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Optional human-readable title for the tool. + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// Optional tool category discriminator. + [JsonPropertyName("type")] + public string? Type { get; set; } } -/// Result of the queued command execution. -/// Data type discriminated by handled. +/// Rust-owned built-in tool descriptors for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class QueuedCommandResult +public sealed class ToolsGetBuiltinDescriptorsResult { - /// The boolean discriminator. - [JsonPropertyName("handled")] - public bool Handled { get; set; } - - /// When true, the runtime will not process subsequent queued commands until a new request comes in. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("stopProcessingQueue")] - public bool? StopProcessingQueue { get; set; } + /// Built-in tool descriptors materialized for the session. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } } -/// Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). +/// Shell-specific names and description lines used to materialize built-in shell tool descriptors. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsRespondToQueuedCommandRequest +public sealed class ToolsShellDescriptorConfig { - /// Request ID from the `command.queued` event the host is responding to. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Additional model-facing shell description lines. + [JsonPropertyName("descriptionLines")] + public IList DescriptionLines { get => field ??= []; set; } - /// Result of the queued command execution. - [JsonPropertyName("result")] - public QueuedCommandResult Result { get => field ??= new(); set; } + /// Human-readable shell name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Tool name used to list active shells. + [JsonPropertyName("listShellsToolName")] + public string ListShellsToolName { get; set; } = string.Empty; -/// Telemetry engagement ID for the session, when available. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionTelemetryEngagement -{ - /// Current telemetry engagement ID, when available. - [JsonPropertyName("engagementId")] - public string? EngagementId { get; set; } -} + /// Tool name used to read shell output. + [JsonPropertyName("readShellToolName")] + public string ReadShellToolName { get; set; } = string.Empty; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTelemetryGetEngagementIdRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Tool name used to start shell commands. + [JsonPropertyName("shellToolName")] + public string ShellToolName { get; set; } = string.Empty; -/// Feature override key/value pairs to attach to subsequent telemetry events from this session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TelemetrySetFeatureOverridesRequest -{ - /// Override key/value pairs to attach to subsequent telemetry events from this session. Replaces any previously-set overrides. - [JsonPropertyName("features")] - public IDictionary Features { get => field ??= new Dictionary(); set; } + /// Stable shell type identifier. + [JsonPropertyName("shellType")] + public string ShellType { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Tool name used to stop shell commands. + [JsonPropertyName("stopShellToolName")] + public string StopShellToolName { get; set; } = string.Empty; } -/// Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. +/// Options controlling how Rust-owned built-in tool descriptors are materialized. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIEphemeralQueryResult +internal sealed class ToolsGetBuiltinDescriptorsRequest { - /// Answer returned by the model. - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; -} + /// Whether background task completion notifications are enabled. + [JsonPropertyName("backgroundTaskNotificationsEnabled")] + public bool? BackgroundTaskNotificationsEnabled { get; set; } -/// Transient question to answer without adding it to conversation history. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIEphemeralQueryRequest -{ - /// Question to answer from the current conversation context. - [JsonPropertyName("question")] - public string Question { get; set; } = string.Empty; + /// Whether tool descriptors should include authoring metadata. + [JsonPropertyName("includeAuthor")] + public bool? IncludeAuthor { get; set; } + + /// Whether descriptors should favor fewer user-intervention prompts. + [JsonPropertyName("reduceUserIntervention")] + public bool? ReduceUserIntervention { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; -} -/// The elicitation response (accept with form values, decline, or cancel). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIElicitationResponse -{ - /// MCP response metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } + /// Shell-specific names and description lines for shell tools. + [JsonPropertyName("shellConfig")] + public ToolsShellDescriptorConfig? ShellConfig { get; set; } - /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). - [JsonPropertyName("action")] - public UIElicitationResponseAction Action { get; set; } + /// Whether the configured shell supports PowerShell 7 syntax. + [JsonPropertyName("shellSupportsPowerShell7Syntax")] + public bool? ShellSupportsPowerShell7Syntax { get; set; } - /// The form values submitted by the user (present when action is 'accept'). - [JsonPropertyName("content")] - public IDictionary? Content { get; set; } + /// Default shell timeout in milliseconds. + [JsonPropertyName("shellTimeoutMs")] + public double? ShellTimeoutMs { get; set; } + + /// Whether semantic skill lookup is available. + [JsonPropertyName("skillEmbeddingEnabled")] + public bool? SkillEmbeddingEnabled { get; set; } } -/// JSON Schema describing the form fields to present to the user. +/// Task completion notification with summary from the agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIElicitationSchema +public sealed class TaskCompleteData { - /// Form field definitions, keyed by field name. - [JsonPropertyName("properties")] - public IDictionary Properties { get => field ??= new Dictionary(); set; } + /// Structured blocker details when outcome is blocked. + [JsonPropertyName("blocker")] + public TaskBlocker? Blocker { get; set; } - /// List of required field names. - [JsonPropertyName("required")] - public IList? Required { get; set; } + /// Active autopilot objective ID evaluated by the completion reviewer. + [JsonPropertyName("objectiveId")] + public long? ObjectiveId { get; set; } - /// Schema type indicator (always 'object'). - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; -} + /// Semantic completion decision. Absent on legacy events and invalid tool calls. + [JsonPropertyName("outcome")] + public TaskCompletionOutcome? Outcome { get; set; } -/// Metadata controlling an MCP task's lifetime. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpTaskMetadata -{ - /// Task time-to-live. - [JsonPropertyName("ttl")] - public long? Ttl { get; set; } + /// Label-safe runtime rationale for the completion decision (e.g. a cancellation or pause/resume downgrade), when one applies. Reviewer-authored rationale is intentionally omitted here because this event has no IFC label channel; the reviewer's findings remain available through its own labeled sub-agent events. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Whether the task was accepted as complete. False when validation failed or completion was rejected or blocked by the reviewer. + [JsonPropertyName("success")] + public bool? Success { get; set; } + + /// Summary of the completed task, provided by the agent. + [JsonPropertyName("summary")] + public string? Summary { get; set; } } -/// Prompt message and JSON schema describing the form fields to elicit from the user. +/// Binary result returned by a tool for the model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIElicitationRequest +public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm { - /// MCP request metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } - - /// Message describing what information is needed from the user. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Base64-encoded binary data. + [Base64String] + [JsonPropertyName("data")] + public string Data { get; set; } = string.Empty; - /// Elicitation mode. Omitted and form are equivalent for structured elicitation. - [JsonPropertyName("mode")] - public McpElicitationFormMode? Mode { get; set; } + /// Human-readable description of the binary data. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// JSON Schema describing the form fields to present to the user. - [JsonPropertyName("requestedSchema")] - public UIElicitationSchema RequestedSchema { get => field ??= new(); set; } + /// Optional metadata from the producing tool. + [JsonPropertyName("metadata")] + public IDictionary? Metadata { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// MIME type of the binary data. + [JsonPropertyName("mimeType")] + public string MimeType { get; set; } = string.Empty; - /// MCP task metadata. - [JsonPropertyName("task")] - public McpTaskMetadata? Task { get; set; } + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. + [JsonPropertyName("type")] + public ExternalToolTextResultForLlmBinaryResultsForLlmType Type { get; set; } } -/// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. +/// A content block within a tool result, which may be text, terminal output, image, audio, or a resource. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIElicitationResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentText), "text")] +[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentTerminal), "terminal")] +[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentShellExit), "shell_exit")] +[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentImage), "image")] +[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentAudio), "audio")] +[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentResourceLink), "resource_link")] +[JsonDerivedType(typeof(ExternalToolTextResultForLlmContentResource), "resource")] +public partial class ExternalToolTextResultForLlmContent { - /// Whether the response was accepted. False if the request was already resolved by another client. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + +/// Plain text content block. +/// The text variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingElicitationRequest +public partial class ExternalToolTextResultForLlmContentText : ExternalToolTextResultForLlmContent { - /// The unique request ID from the elicitation.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; - - /// The elicitation response (accept with form values, decline, or cancel). - [JsonPropertyName("result")] - public UIElicitationResponse Result { get => field ??= new(); set; } + /// + [JsonIgnore] + public override string Type => "text"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The text content. + [JsonPropertyName("text")] + public required string Text { get; set; } } -/// Indicates whether the pending UI request was resolved by this call. +/// Terminal/shell output content block with optional exit code and working directory. +/// The terminal variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIHandlePendingResult +public partial class ExternalToolTextResultForLlmContentTerminal : ExternalToolTextResultForLlmContent { - /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// + [JsonIgnore] + public override string Type => "terminal"; + + /// Working directory where the command was executed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } + + /// Process exit code, if the command has completed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Terminal/shell output text. + [JsonPropertyName("text")] + public required string Text { get; set; } } -/// User response for a pending user-input request, with answer text and whether it was typed freeform. +/// Shell command exit metadata with optional output preview. +/// The shell_exit variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIUserInputResponse +public partial class ExternalToolTextResultForLlmContentShellExit : ExternalToolTextResultForLlmContent { - /// The user's answer text. - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "shell_exit"; - /// True if the user typed a freeform response, false if they selected a presented choice. Used by telemetry to differentiate between free text input and choice selection. - [JsonPropertyName("wasFreeform")] - public bool WasFreeform { get; set; } + /// Working directory where the shell command was executed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } + + /// Exit code from the completed shell command. + [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")] + public string? OutputPreview { get; set; } + + /// Whether outputPreview is known to be incomplete or truncated. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("outputTruncated")] + public bool? OutputTruncated { get; set; } + + /// Shell id, as assigned by Copilot runtime. + [JsonPropertyName("shellId")] + public required string ShellId { get; set; } } -/// Request ID of a pending `user_input.requested` event and the user's response. +/// Image content block with base64-encoded data. +/// The image variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingUserInputRequest +public partial class ExternalToolTextResultForLlmContentImage : ExternalToolTextResultForLlmContent { - /// The unique request ID from the user_input.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "image"; - /// User response for a pending user-input request, with answer text and whether it was typed freeform. - [JsonPropertyName("response")] - public UIUserInputResponse Response { get => field ??= new(); set; } + /// Base64-encoded image data. + [Base64String] + [JsonPropertyName("data")] + public required string Data { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// MIME type of the image (e.g., image/png, image/jpeg). + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } } -/// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. +/// Audio content block with base64-encoded data. +/// The audio variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIHandlePendingSamplingResponse +public partial class ExternalToolTextResultForLlmContentAudio : ExternalToolTextResultForLlmContent { + /// + [JsonIgnore] + public override string Type => "audio"; + + /// Base64-encoded audio data. + [Base64String] + [JsonPropertyName("data")] + public required string Data { get; set; } + + /// MIME type of the audio (e.g., audio/wav, audio/mpeg). + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } } -/// Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). +/// Icon image for a resource. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingSamplingRequest +public sealed class ExternalToolTextResultForLlmContentResourceLinkIcon { - /// The unique request ID from the sampling.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// MIME type of the icon image. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. - [JsonPropertyName("response")] - public UIHandlePendingSamplingResponse? Response { get; set; } + /// Available icon sizes (e.g., ['16x16', '32x32']). + [JsonPropertyName("sizes")] + public IList? Sizes { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// URL or path to the icon image. + [JsonPropertyName("src")] + public string Src { get; set; } = string.Empty; + + /// Theme variant this icon is intended for. + [JsonPropertyName("theme")] + public ExternalToolTextResultForLlmContentResourceLinkIconTheme? Theme { get; set; } } -/// Request ID of a pending `auto_mode_switch.requested` event and the user's response. +/// Resource link content block referencing an external resource. +/// The resource_link variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingAutoModeSwitchRequest +public partial class ExternalToolTextResultForLlmContentResourceLink : ExternalToolTextResultForLlmContent { - /// The unique request ID from the auto_mode_switch.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "resource_link"; - /// User's choice for auto-mode switching: yes (allow this turn), yes_always (allow + persist as setting), or no (decline). - [JsonPropertyName("response")] - public UIAutoModeSwitchResponse Response { get; set; } + /// Human-readable description of the resource. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Icons associated with this resource. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("icons")] + public IList? Icons { get; set; } + + /// MIME type of the resource content. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// Resource name identifier. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Size of the resource in bytes. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("size")] + public long? Size { get; set; } + + /// Human-readable display title for the resource. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// URI identifying the resource. + [JsonPropertyName("uri")] + public required string Uri { get; set; } } -/// The user's selected action for an exhausted session limit. +/// Embedded resource content block with inline text or binary data. +/// The resource variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UISessionLimitsExhaustedResponse +public partial class ExternalToolTextResultForLlmContentResource : ExternalToolTextResultForLlmContent { - /// Action selected by the user. - [JsonPropertyName("action")] - public UISessionLimitsExhaustedResponseAction Action { get; set; } + /// + [JsonIgnore] + public override string Type => "resource"; - /// AI Credits to add to the current max when action is 'add'. - [JsonPropertyName("additionalAiCredits")] - public double? AdditionalAiCredits { get; set; } + /// The embedded resource contents, either text or base64-encoded binary. + [JsonPropertyName("resource")] + public required JsonElement Resource { get; set; } +} - /// New absolute max AI Credits when action is 'set'. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; set; } +/// A message injected by a tool result. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ToolResultNewMessage +{ + /// Message content to inject after the tool result. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Source attributed to the injected message. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; } -/// Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. +/// RPC data type for TaskCompletionDecision operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingSessionLimitsExhaustedRequest +public sealed class TaskCompletionDecision { - /// The unique request ID from the session_limits_exhausted.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Objective eligibility token captured when the decision was evaluated. + [JsonPropertyName("completionEligibilityToken")] + public long? CompletionEligibilityToken { get; set; } - /// The selected session-limit action. - [JsonPropertyName("response")] - public UISessionLimitsExhaustedResponse Response { get => field ??= new(); set; } + /// Whether completion was accepted after the reviewer-rejection budget was exhausted. + [JsonPropertyName("completionRejectionBudgetExhausted")] + public bool? CompletionRejectionBudgetExhausted { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Active autopilot objective evaluated by the completion reviewer. + [JsonPropertyName("objectiveId")] + public long? ObjectiveId { get; set; } + + /// Semantic result of evaluating the task completion request. + [JsonPropertyName("outcome")] + public TaskCompletionOutcome Outcome { get; set; } + + /// Rationale for the completion decision, when one is available. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Whether the rationale was derived from completion-reviewer output. + [JsonPropertyName("reviewerDerived")] + public bool? ReviewerDerived { get; set; } + + /// Information-flow metadata captured from the completion reviewer. + [JsonPropertyName("reviewerResultMeta")] + public JsonElement? ReviewerResultMeta { get; set; } } -/// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. +/// Expanded canonical result returned by a session tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIExitPlanModeResponse +public sealed class ToolResultExpanded { - /// Whether the plan was approved. - [JsonPropertyName("approved")] - public bool Approved { get; set; } + /// Base64-encoded binary results returned to the model. + [JsonPropertyName("binaryResultsForLlm")] + public IList? BinaryResultsForLlm { get; set; } - /// Whether subsequent edits should be auto-approved without confirmation. - [JsonPropertyName("autoApproveEdits")] - public bool? AutoApproveEdits { get; set; } + /// Sources returned by the tool that the model may cite. + [JsonPropertyName("citableSources")] + public IList? CitableSources { get; set; } - /// When true, the agent is instructed to end its turn without starting implementation so the client can restore the session model and auto-submit a fresh implementation turn on it. Set only when a distinct plan configuration (a different model, reasoning effort, or context tier) actually ran the planning turn. - [JsonPropertyName("deferImplementation")] - public bool? DeferImplementation { get; set; } + /// Structured content blocks returned to the model. + [JsonPropertyName("contents")] + public IList? Contents { get; set; } - /// Feedback from the user when they declined the plan or requested changes. - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } + /// Error message for an unsuccessful execution. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. - [JsonPropertyName("selectedAction")] - public UIExitPlanModeAction? SelectedAction { get; set; } + /// Metadata propagated with the tool result, including information-flow labels. + [JsonPropertyName("mcpMeta")] + public IDictionary? McpMeta { get; set; } + + /// Messages to inject after the tool result. + [JsonPropertyName("newMessages")] + public IList? NewMessages { get; set; } + + /// Whether post-tool-use failure hooks have already processed this result. + [JsonPropertyName("postToolUseFailureHooksProcessed")] + public bool? PostToolUseFailureHooksProcessed { get; set; } + + /// Execution outcome classification. + [JsonPropertyName("resultType")] + public ToolResultType ResultType { get; set; } + + /// Detailed log content available for session display. + [JsonPropertyName("sessionLog")] + public string? SessionLog { get; set; } + + /// Skill invocation metadata produced by the tool. + [JsonPropertyName("skillInvocation")] + public JsonElement? SkillInvocation { get; set; } + + /// Whether large-output post-processing should be skipped. + [JsonPropertyName("skipLargeOutputProcessing")] + public bool? SkipLargeOutputProcessing { get; set; } + + /// Structured result content in addition to the model-facing text. + [JsonPropertyName("structuredContent")] + public JsonElement? StructuredContent { get; set; } + + /// Completion-review decision produced by the task-completion tool. + [JsonPropertyName("taskCompletionDecision")] + public TaskCompletionDecision? TaskCompletionDecision { get; set; } + + /// Text result returned to the model. + [JsonPropertyName("textResultForLlm")] + public string TextResultForLlm { get; set; } = string.Empty; + + /// Deferred tool names made available by this result. + [JsonPropertyName("toolReferences")] + public IList? ToolReferences { get; set; } + + /// Tool-specific telemetry payload. + [JsonPropertyName("toolTelemetry")] + public JsonElement? ToolTelemetry { get; set; } + + /// Optional UI resource produced by the tool. + [JsonPropertyName("uiResource")] + public JsonElement? UiResource { get; set; } } -/// Request ID of a pending `exit_plan_mode.requested` event and the user's response. +/// Task-completion tool arguments and final result used to build a label-safe session event payload. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingExitPlanModeRequest +internal sealed class ToolsTaskCompleteEventDataRequest { - /// The unique request ID from the exit_plan_mode.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; - - /// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. - [JsonPropertyName("response")] - public UIExitPlanModeResponse Response { get => field ??= new(); set; } + /// Final expanded result returned by the task_complete tool. + [JsonPropertyName("finalResult")] + public ToolResultExpanded FinalResult { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Arguments supplied to the completed task_complete tool call. + [JsonPropertyName("toolArgs")] + public JsonElement ToolArgs { get; set; } } -/// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). +/// Indicates whether the external tool call result was handled successfully. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIRegisterDirectAutoModeSwitchHandlerResult +public sealed class HandlePendingToolCallResult { - /// Opaque handle representing the registration. Pass this same handle to `unregisterDirectAutoModeSwitchHandler` when the in-process handler is no longer active. Multiple registrations are reference-counted; the server bridge will only dispatch auto-mode-switch requests when no handles are active. - [JsonPropertyName("handle")] - public string Handle { get; set; } = string.Empty; + /// Whether the tool call result was handled successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Identifies the target session. +/// Pending external tool call request ID, with the tool result or an error describing why it failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUiRegisterDirectAutoModeSwitchHandlerRequest +internal sealed class HandlePendingToolCallRequest { + /// Error message if the tool call failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Request ID of the pending tool call. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Tool call result (string or expanded result object). + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the handle was active and the registration count was decremented. +/// Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIUnregisterDirectAutoModeSwitchHandlerResult +public sealed class ToolsInitializeAndValidateResult { - /// True if the handle was active and decremented the counter; false if the handle was unknown. - [JsonPropertyName("unregistered")] - public bool Unregistered { get; set; } } -/// Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIUnregisterDirectAutoModeSwitchHandlerRequest +internal sealed class SessionToolsInitializeAndValidateRequest { - /// Handle previously returned by `registerDirectAutoModeSwitchHandler`. - [JsonPropertyName("handle")] - public string Handle { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// Lightweight metadata for a currently initialized session tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureResult +public sealed class CurrentToolMetadata { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} - -/// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRuleSource -{ - /// Name of the policy source. + /// Whether the tool is loaded on demand via tool search. + [JsonPropertyName("deferLoading")] + public bool? DeferLoading { get; set; } + + /// Tool description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// JSON Schema for tool input. + [JsonPropertyName("input_schema")] + public IDictionary? InputSchema { get; set; } + + /// MCP server name for MCP-backed tools. + [JsonPropertyName("mcpServerName")] + public string? McpServerName { get; set; } + + /// Raw MCP tool name for MCP-backed tools. + [JsonPropertyName("mcpToolName")] + public string? McpToolName { get; set; } + + /// Model-facing tool name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Type of the policy source. - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; + /// Optional MCP/config namespaced tool name. + [JsonPropertyName("namespacedName")] + public string? NamespacedName { get; set; } } -/// Single content-exclusion rule supplied to `session.permissions.configure`, with paths, match conditions, and source. +/// Current lightweight tool metadata snapshot for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRule +public sealed class ToolsGetCurrentMetadataResult { - /// Conditions of which at least one must match. - [JsonPropertyName("ifAnyMatch")] - public IList? IfAnyMatch { get; set; } - - /// Conditions none of which may match. - [JsonPropertyName("ifNoneMatch")] - public IList? IfNoneMatch { get; set; } - - /// Path patterns covered by this rule. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } - - /// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. - [JsonPropertyName("source")] - public PermissionsConfigureAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } + /// Current tool metadata, or null when tools have not been initialized yet. + [JsonPropertyName("tools")] + public IList? Tools { get; set; } } -/// Content-exclusion policy supplied to `session.permissions.configure`, with rules, last-updated data, and scope. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicy +internal sealed class SessionToolsGetCurrentMetadataRequest { - /// Opaque policy update timestamp supplied by the host. - [JsonPropertyName("last_updated_at")] - public JsonElement LastUpdatedAt { get; set; } - - /// Content-exclusion rules to apply. - [JsonPropertyName("rules")] - public IList Rules { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. - [JsonPropertyName("scope")] - public PermissionsConfigureAdditionalContentExclusionPolicyScope Scope { get; set; } +/// Empty result after replacing the calling connection's externally implemented tools. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ToolsSetResult +{ } -/// If specified, replaces the session's path-permission policy. The runtime constructs the appropriate PathManager based on these inputs (rooted at the session's working directory). Omit to leave the current path policy unchanged. +/// Serializable definition of a caller-implemented tool whose execution is handled over the SDK connection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsConfig +public sealed class ProtocolExternalToolDefinition { - /// 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; } + /// Tool-loading deferral policy. + [JsonPropertyName("defer")] + public ProtocolExternalToolDefer? Defer { get; set; } - /// Whether to include the system temp directory in the allowed list (defaults to true). Ignored when `unrestricted` is true. - [JsonPropertyName("includeTempDirectory")] - public bool? IncludeTempDirectory { get; set; } + /// Model-visible explanation of what the tool does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// If true, the runtime allows access to all paths without prompting. Equivalent to constructing an UnrestrictedPathManager. - [JsonPropertyName("unrestricted")] - public bool? Unrestricted { get; set; } + /// Whether the tool executes commands in a terminal. + [JsonPropertyName("isTerminal")] + public bool? IsTerminal { get; set; } - /// Workspace root path (special-cased to be allowed even before the directory exists). Ignored when `unrestricted` is true. - [JsonPropertyName("workspacePath")] - public string? WorkspacePath { get; set; } + /// Optional caller-defined metadata associated with the tool. + [JsonPropertyName("metadata")] + public IDictionary? Metadata { get; set; } + + /// Unique model-visible tool name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Whether this definition replaces a built-in tool with the same name. + [JsonPropertyName("overridesBuiltInTool")] + public bool? OverridesBuiltInTool { get; set; } + + /// JSON Schema describing the tool's input arguments. + [JsonPropertyName("parameters")] + public IDictionary? Parameters { get; set; } + + /// Whether execution bypasses the normal tool permission prompt. + [JsonPropertyName("skipPermission")] + public bool? SkipPermission { get; set; } + + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } } -/// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. +/// Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionRulesSet +internal sealed class ToolsSetRequest { - /// Rules that auto-approve matching requests. - [JsonPropertyName("approved")] - public IList Approved { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Rules that auto-deny matching requests. - [JsonPropertyName("denied")] - public IList Denied { get => field ??= []; set; } + /// Complete replacement list for the calling connection. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } } -/// If specified, replaces the session's URL-permission policy. The runtime constructs a fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy unchanged. +/// Empty result after applying subagent settings. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionUrlsConfig +public sealed class ToolsUpdateSubagentSettingsResult { - /// Initial list of allowed URL/domain patterns. Patterns may include path components. Ignored when `unrestricted` is true. - [JsonPropertyName("initialAllowed")] - public IList? InitialAllowed { get; set; } - - /// If true, the runtime allows access to all URLs without prompting. Initial allow-list is ignored when this is true. - [JsonPropertyName("unrestricted")] - public bool? Unrestricted { get; set; } } -/// Patch of permission policy fields to apply (omit a field to leave it unchanged). +/// Subagent model, reasoning effort, context tier, and auto-invocation settings. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsConfigureParams +public sealed class SubagentSettingsEntry { - /// If specified, replaces the host-supplied GitHub Content Exclusion policies on the session (combined with natively-discovered policies when evaluating tool/file access). Omit to leave the current policies unchanged. - [JsonPropertyName("additionalContentExclusionPolicies")] - public IList? AdditionalContentExclusionPolicies { get; set; } + /// Whether this agent's runtime-defined proactive invocation prompting is enabled, if supported. Currently consumed by the built-in rubber-duck agent. + [JsonPropertyName("autoInvoke")] + public bool? AutoInvoke { get; set; } - /// If specified, sets whether path/URL read permission requests are auto-approved. Omit to leave the current value unchanged. - [JsonPropertyName("approveAllReadPermissionRequests")] - public bool? ApproveAllReadPermissionRequests { get; set; } + /// Context tier override for matching subagents. + [JsonPropertyName("contextTier")] + public SubagentSettingsEntryContextTier? ContextTier { get; set; } - /// If specified, sets whether tool permission requests are auto-approved without prompting. Omit to leave the current value unchanged. - [JsonPropertyName("approveAllToolPermissionRequests")] - public bool? ApproveAllToolPermissionRequests { get; set; } + /// Reasoning effort override for matching subagents. + [JsonPropertyName("effortLevel")] + public string? EffortLevel { get; set; } - /// If specified, replaces the session's path-permission policy. The runtime constructs the appropriate PathManager based on these inputs (rooted at the session's working directory). Omit to leave the current path policy unchanged. - [JsonPropertyName("paths")] - public PermissionPathsConfig? Paths { get; set; } + /// Model override for matching subagents. + [JsonPropertyName("model")] + public string? Model { get; set; } - /// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. - [JsonPropertyName("rules")] - public PermissionRulesSet? Rules { get; set; } + /// Whether the configured model strategy is preferred or required. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } +} + +/// Configured per-agent subagent overrides. +public sealed class UpdateSubagentSettingsRequestSubagents +{ + /// Per-agent settings keyed by subagent agent_type. + [JsonPropertyName("agents")] + public IDictionary? Agents { get; set; } + /// Names of subagents the user has turned off; they cannot be dispatched. + [JsonPropertyName("disabledSubagents")] + public IList? DisabledSubagents { get; set; } + + /// Maximum number of subagents that can run concurrently; applies to usage-based billing users only. + [JsonPropertyName("maxConcurrency")] + public int? MaxConcurrency { get; set; } + + /// Maximum subagent nesting depth; applies to usage-based billing users only. + [JsonPropertyName("maxDepth")] + public int? MaxDepth { get; set; } +} + +/// Subagent settings to apply to the current session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class UpdateSubagentSettingsRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// If specified, replaces the session's URL-permission policy. The runtime constructs a fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy unchanged. - [JsonPropertyName("urls")] - public PermissionUrlsConfig? Urls { get; set; } + /// Subagent settings to apply, or null to clear the live session override. + [JsonPropertyName("subagents")] + public UpdateSubagentSettingsRequestSubagents? Subagents { get; set; } } -/// Indicates whether the permission decision was applied; false when the request was already resolved. +/// RPC data type for SessionCommandsList operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionRequestResult +public sealed class SessionCommandsListRequest { - /// Whether the permission request was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Include runtime built-in commands. + [JsonPropertyName("includeBuiltins")] + public bool? IncludeBuiltins { get; set; } + + /// Include commands registered by protocol clients, including SDK clients and extensions. + [JsonPropertyName("includeClientCommands")] + public bool? IncludeClientCommands { get; set; } + + /// Include enabled user-invocable skills and commands. + [JsonPropertyName("includeSkills")] + public bool? IncludeSkills { get; set; } } -/// The client's response to the pending permission prompt. -/// Polymorphic base type discriminated by kind. +/// RPC data type for SessionCommandsListRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(PermissionDecisionApproveOnce), "approve-once")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSession), "approve-for-session")] -[JsonDerivedType(typeof(PermissionDecisionApproveReadOnlyForSession), "approve-read-only-for-session")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocation), "approve-for-location")] -[JsonDerivedType(typeof(PermissionDecisionApprovePermanently), "approve-permanently")] -[JsonDerivedType(typeof(PermissionDecisionReject), "reject")] -[JsonDerivedType(typeof(PermissionDecisionUserNotAvailable), "user-not-available")] -[JsonDerivedType(typeof(PermissionDecisionApproved), "approved")] -[JsonDerivedType(typeof(PermissionDecisionApprovedForSession), "approved-for-session")] -[JsonDerivedType(typeof(PermissionDecisionApprovedForLocation), "approved-for-location")] -[JsonDerivedType(typeof(PermissionDecisionCancelled), "cancelled")] -[JsonDerivedType(typeof(PermissionDecisionDeniedByRules), "denied-by-rules")] -[JsonDerivedType(typeof(PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser), "denied-no-approval-rule-and-could-not-request-from-user")] -[JsonDerivedType(typeof(PermissionDecisionDeniedInteractivelyByUser), "denied-interactively-by-user")] -[JsonDerivedType(typeof(PermissionDecisionDeniedByContentExclusionPolicy), "denied-by-content-exclusion-policy")] -[JsonDerivedType(typeof(PermissionDecisionDeniedByPermissionRequestHook), "denied-by-permission-request-hook")] -public partial class PermissionDecision +internal sealed class SessionCommandsListRequestWithSession { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Include runtime built-in commands. + [JsonPropertyName("includeBuiltins")] + public bool? IncludeBuiltins { get; set; } + /// Include commands registered by protocol clients, including SDK clients and extensions. + [JsonPropertyName("includeClientCommands")] + public bool? IncludeClientCommands { get; set; } -/// Permission-decision request variant to approve only the current permission request. -/// The approve-once variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveOnce : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "approve-once"; + /// Include enabled user-invocable skills and commands. + [JsonPropertyName("includeSkills")] + public bool? IncludeSkills { get; set; } - /// True only when a host surfaced this request to a user who approved it. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approvedInteractively")] - public bool? ApprovedInteractively { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). +/// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). /// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalCommands), "commands")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalRead), "read")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalWrite), "write")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalMcp), "mcp")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalMcpSampling), "mcp-sampling")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalMemory), "memory")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalCustomTool), "custom-tool")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalExtensionManagement), "extension-management")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalWorkflow), "workflow")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess), "extension-permission-access")] -[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalExtensionEnvAccess), "extension-env-access")] -public partial class PermissionDecisionApproveForSessionApproval +[JsonDerivedType(typeof(SlashCommandInvocationResultText), "text")] +[JsonDerivedType(typeof(SlashCommandInvocationResultAgentPrompt), "agent-prompt")] +[JsonDerivedType(typeof(SlashCommandInvocationResultCompleted), "completed")] +[JsonDerivedType(typeof(SlashCommandInvocationResultSelectSubcommand), "select-subcommand")] +[JsonDerivedType(typeof(SlashCommandInvocationResultAddTimelineEntry), "add-timeline-entry")] +[JsonDerivedType(typeof(SlashCommandInvocationResultShowDialog), "show-dialog")] +[JsonDerivedType(typeof(SlashCommandInvocationResultSetModel), "set-model")] +[JsonDerivedType(typeof(SlashCommandInvocationResultSetPlanModel), "set-plan-model")] +public partial class SlashCommandInvocationResult { /// The type discriminator. [JsonPropertyName("kind")] @@ -20720,771 +21305,822 @@ public partial class PermissionDecisionApproveForSessionApproval } -/// Session-scoped approval details for specific command identifiers. -/// The commands variant of . +/// Slash-command invocation result containing text output plus Markdown/ANSI rendering flags. +/// The text variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalCommands : PermissionDecisionApproveForSessionApproval +public partial class SlashCommandInvocationResultText : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "commands"; + public override string Kind => "text"; - /// Command identifiers covered by this approval. - [JsonPropertyName("commandIdentifiers")] - public required IList CommandIdentifiers { get; set; } -} + /// Whether text contains Markdown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("markdown")] + public bool? Markdown { get; set; } -/// Session-scoped approval details for read-only filesystem operations. -/// The read variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalRead : PermissionDecisionApproveForSessionApproval -{ - /// - [JsonIgnore] - public override string Kind => "read"; + /// Whether ANSI sequences should be preserved. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("preserveAnsi")] + public bool? PreserveAnsi { get; set; } + + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } + + /// Present when the invocation changed the sandbox for this session only. Nothing was persisted, so consumers must mirror the change onto the live session rather than reloading settings, and must not treat it as a settings change. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sandboxSessionChange")] + public SandboxSessionChange? SandboxSessionChange { get; set; } + + /// Text output for the client to render. + [JsonPropertyName("text")] + public required string Text { get; set; } } -/// Session-scoped approval details for filesystem write operations. -/// The write variant of . +/// Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. +/// The agent-prompt variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalWrite : PermissionDecisionApproveForSessionApproval +public partial class SlashCommandInvocationResultAgentPrompt : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "write"; + public override string Kind => "agent-prompt"; + + /// Prompt text to display to the user. + [JsonPropertyName("displayPrompt")] + public required string DisplayPrompt { get; set; } + + /// Optional target session mode for the agent prompt. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mode")] + public SessionMode? Mode { get; set; } + + /// Optional user-facing notice to show before the prompt is submitted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("notice")] + public string? Notice { get; set; } + + /// Prompt to submit to the agent. + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } + + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } } -/// Session-scoped approval details for an MCP server tool, or all tools on the server when `toolName` is null. -/// The mcp variant of . +/// Slash-command invocation result indicating completion, with optional message and settings-change flag. +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalMcp : PermissionDecisionApproveForSessionApproval +public partial class SlashCommandInvocationResultCompleted : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "mcp"; + public override string Kind => "completed"; - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Optional user-facing message describing the completed command. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } + /// Optional target session mode applied without submitting an agent prompt. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mode")] + public SessionMode? Mode { get; set; } + + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } } -/// Session-scoped approval details for MCP sampling requests from a server. -/// The mcp-sampling variant of . +/// Selectable slash-command subcommand option with name, description, and optional group label. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalMcpSampling : PermissionDecisionApproveForSessionApproval +public sealed class SlashCommandSelectSubcommandOption { - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; + /// Human-readable description of the subcommand. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Optional group label for organizing options. + [JsonPropertyName("group")] + public string? Group { get; set; } + + /// Subcommand name to invoke. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Session-scoped approval details for writes to long-term memory. -/// The memory variant of . +/// Slash-command invocation result asking the client to present subcommand options for a parent command. +/// The select-subcommand variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalMemory : PermissionDecisionApproveForSessionApproval +public partial class SlashCommandInvocationResultSelectSubcommand : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "memory"; + public override string Kind => "select-subcommand"; + + /// Parent command name that requires subcommand selection. + [JsonPropertyName("command")] + public required string Command { get; set; } + + /// Available subcommand options for the client to present. + [JsonPropertyName("options")] + public required IList Options { get; set; } + + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } + + /// Human-readable title for the selection UI. + [JsonPropertyName("title")] + public required string Title { get; set; } } -/// Session-scoped approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// RPC data type for SlashCommandTimelineEntry operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalCustomTool : PermissionDecisionApproveForSessionApproval +public sealed class SlashCommandTimelineEntry { - /// - [JsonIgnore] - public override string Kind => "custom-tool"; + /// What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { get; set; } + /// Text displayed for the timeline entry. + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + + /// Timeline entry presentation type. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + /// Optional URL associated with the timeline entry. + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// Session-scoped approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// The add-timeline-entry variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionManagement : PermissionDecisionApproveForSessionApproval +public partial class SlashCommandInvocationResultAddTimelineEntry : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "extension-management"; + public override string Kind => "add-timeline-entry"; - /// Optional operation identifier; when omitted, the approval covers all extension management operations. + /// Timeline entry the host should append. + [JsonPropertyName("entry")] + public required SlashCommandTimelineEntry Entry { get; set; } + + /// Optional text the host should prefill into the input editor. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("operation")] - public string? Operation { get; set; } + [JsonPropertyName("prefillInput")] + public string? PrefillInput { get; set; } + + /// Whether command execution changed persisted runtime settings. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } } -/// Session-scoped workflow approval, optionally narrowed by approval key. -/// The workflow variant of . +/// RPC data type for SlashCommandModelPickerDialog operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalWorkflow : PermissionDecisionApproveForSessionApproval +public sealed class SlashCommandModelPickerDialog { - /// - [JsonIgnore] - public override string Kind => "workflow"; + /// Discriminator for a model-picker dialog. + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; - /// Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approvalKey")] - public string? ApprovalKey { get; set; } -} + /// Model that should be enabled before it can be selected. + [JsonPropertyName("modelToEnable")] + public string? ModelToEnable { get; set; } -/// Session-scoped approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess : PermissionDecisionApproveForSessionApproval -{ - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// Settings scope the picker should modify. + [JsonPropertyName("scope")] + public string? Scope { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } + /// Model-selection target represented by the picker. + [JsonPropertyName("target")] + public string? Target { get; set; } } -/// Session-scoped approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. -/// The extension-env-access variant of . +/// The show-dialog variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionEnvAccess : PermissionDecisionApproveForSessionApproval +public partial class SlashCommandInvocationResultShowDialog : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "extension-env-access"; + public override string Kind => "show-dialog"; - /// Names of the sensitive environment variables this approval covers. Values are never persisted. - [JsonPropertyName("environmentVariables")] - public required IList EnvironmentVariables { get; set; } + /// Dialog the host should display. + [JsonPropertyName("dialog")] + public required SlashCommandModelPickerDialog Dialog { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } + /// Whether command execution changed persisted runtime settings. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } } -/// Permission-decision request variant to approve for the rest of the session, with optional tool approval or URL domain. -/// The approve-for-session variant of . +/// User-settings snapshot to restore if the host cancels the model switch. +public sealed class SlashCommandInvocationResultSetModelRevertOnCancel +{ +} + +/// The set-model variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSession : PermissionDecision +public partial class SlashCommandInvocationResultSetModel : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "approve-for-session"; + public override string Kind => "set-model"; - /// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). + /// Auto routing profile selected by the command, when the model is Auto. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approval")] - public PermissionDecisionApproveForSessionApproval? Approval { get; set; } + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } - /// URL domain to approve for the rest of the session (URL prompts only). + /// Model selected by the command. + [JsonPropertyName("model")] + public required string Model { get; set; } + + /// Reasoning effort selected for the model. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("domain")] - public string? Domain { get; set; } + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + + /// Repository settings scope modified by the command. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("repoScope")] + public string? RepoScope { get; set; } + + /// User-settings snapshot to restore if the host cancels the model switch. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("revertOnCancel")] + public SlashCommandInvocationResultSetModelRevertOnCancel? RevertOnCancel { get; set; } + + /// Whether command execution changed persisted runtime settings. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } + + /// Settings scope modified by the command. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + /// User-facing warning produced while selecting the model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Approve file-tool read access to specific directories for this logical session, including continuation or resume. -/// The approve-read-only-for-session variant of . +/// The set-plan-model variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveReadOnlyForSession : PermissionDecision +public partial class SlashCommandInvocationResultSetPlanModel : SlashCommandInvocationResult { /// [JsonIgnore] - public override string Kind => "approve-read-only-for-session"; + public override string Kind => "set-plan-model"; - /// Canonical directories covered by the read-only grant. - [JsonPropertyName("directories")] - public required IList Directories { get; set; } + /// User-facing confirmation message for the plan-model selection. + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// Dedicated model selected for plan mode. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("planModel")] + public string? PlanModel { get; set; } + + /// Whether command execution changed persisted runtime settings. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } } -/// Approval to persist for this location. -/// Polymorphic base type discriminated by kind. +/// Slash command name and optional raw input string to invoke. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalCommands), "commands")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalRead), "read")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalWrite), "write")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalMcp), "mcp")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalMcpSampling), "mcp-sampling")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalMemory), "memory")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalCustomTool), "custom-tool")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionManagement), "extension-management")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalWorkflow), "workflow")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess), "extension-permission-access")] -[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionEnvAccess), "extension-env-access")] -public partial class PermissionDecisionApproveForLocationApproval +internal sealed class CommandsInvokeRequest { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Raw input after the command name. + [JsonPropertyName("input")] + public string? Input { get; set; } + /// Command name. Leading slashes are stripped and the name is matched case-insensitively. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; -/// Location-scoped approval details for specific command identifiers. -/// The commands variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalCommands : PermissionDecisionApproveForLocationApproval -{ - /// - [JsonIgnore] - public override string Kind => "commands"; + /// Optional client surface that initiated the invocation. + [JsonPropertyName("origin")] + public CommandsInvocationOrigin? Origin { get; set; } - /// Command identifiers covered by this approval. - [JsonPropertyName("commandIdentifiers")] - public required IList CommandIdentifiers { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-scoped approval details for read-only filesystem operations. -/// The read variant of . +/// Whether finalizing the invocation effect succeeded, and the failure reason when it did not. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalRead : PermissionDecisionApproveForLocationApproval +internal sealed class CommandsFinalizeInvocationEffectResult { - /// - [JsonIgnore] - public override string Kind => "read"; + /// Failure reason when the invocation effect could not be finalized. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Whether the pending invocation effect was finalized successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Location-scoped approval details for filesystem write operations. -/// The write variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalWrite : PermissionDecisionApproveForLocationApproval +/// The slash-command result object that produced the pending effect, echoed back unchanged. +public sealed class CommandsFinalizeInvocationEffectRequestEffect { - /// - [JsonIgnore] - public override string Kind => "write"; } -/// Location-scoped approval details for an MCP server tool, or all tools on the server when `toolName` is null. -/// The mcp variant of . +/// The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMcp : PermissionDecisionApproveForLocationApproval +internal sealed class CommandsFinalizeInvocationEffectRequest { - /// - [JsonIgnore] - public override string Kind => "mcp"; + /// The slash-command result object that produced the pending effect, echoed back unchanged. + [JsonPropertyName("effect")] + public CommandsFinalizeInvocationEffectRequestEffect Effect { get => field ??= new(); set; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Whether the host applied or cancelled the pending invocation effect. + [JsonPropertyName("outcome")] + public CommandsInvocationEffectOutcome Outcome { get; set; } - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-scoped approval details for MCP sampling requests from a server. -/// The mcp-sampling variant of . +/// Indicates whether the pending client-handled command was completed successfully. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMcpSampling : PermissionDecisionApproveForLocationApproval +public sealed class CommandsHandlePendingCommandResult { - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; - - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Whether the command was handled successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Location-scoped approval details for writes to long-term memory. -/// The memory variant of . +/// Pending command request ID and an optional error if the client handler failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMemory : PermissionDecisionApproveForLocationApproval +internal sealed class CommandsHandlePendingCommandRequest { - /// - [JsonIgnore] - public override string Kind => "memory"; + /// Error message if the command handler failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Request ID from the command invocation event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-scoped approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Error message produced while executing the command, if any. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalCustomTool : PermissionDecisionApproveForLocationApproval +public sealed class ExecuteCommandResult { - /// - [JsonIgnore] - public override string Kind => "custom-tool"; - - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { get; set; } + /// Error message produced while executing the command, if any. Omitted when the handler succeeded. + [JsonPropertyName("error")] + public string? Error { get; set; } } -/// Location-scoped approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// Slash command name and argument string to execute synchronously. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionManagement : PermissionDecisionApproveForLocationApproval +internal sealed class ExecuteCommandParams { - /// - [JsonIgnore] - public override string Kind => "extension-management"; + /// Argument string to pass to the command (empty string if none). + [JsonPropertyName("args")] + public string Args { get; set; } = string.Empty; - /// Optional operation identifier; when omitted, the approval covers all extension management operations. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("operation")] - public string? Operation { get; set; } + /// Name of the slash command to invoke (without the leading '/'). + [JsonPropertyName("commandName")] + public string CommandName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-scoped workflow approval, optionally narrowed by approval key. -/// The workflow variant of . +/// Indicates whether the command was accepted into the local execution queue. +/// Data type discriminated by queued. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalWorkflow : PermissionDecisionApproveForLocationApproval +public partial class EnqueueCommandResult { - /// - [JsonIgnore] - public override string Kind => "workflow"; + /// The boolean discriminator. + [JsonPropertyName("queued")] + public bool Queued { get; set; } - /// Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. + /// Stable opaque ID of the queued command. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approvalKey")] - public string? ApprovalKey { get; set; } + [JsonPropertyName("queueId")] + public string? QueueId { get; set; } } -/// Location-scoped approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . +/// Slash-prefixed command string to enqueue for FIFO processing. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess : PermissionDecisionApproveForLocationApproval +internal sealed class EnqueueCommandParams { - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// 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. + [JsonPropertyName("command")] + public string Command { get; set; } = string.Empty; - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } + /// 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; } -/// Location-scoped approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. -/// The extension-env-access variant of . +/// Indicates whether the queued-command response was matched to a pending request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionEnvAccess : PermissionDecisionApproveForLocationApproval +public sealed class CommandsRespondToQueuedCommandResult { - /// - [JsonIgnore] - public override string Kind => "extension-env-access"; + /// Whether a pending queued command with the given request ID was found and resolved. False when the request was already resolved, cancelled, or unknown. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Names of the sensitive environment variables this approval covers. Values are never persisted. - [JsonPropertyName("environmentVariables")] - public required IList EnvironmentVariables { get; set; } +/// Result of the queued command execution. +/// Data type discriminated by handled. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class QueuedCommandResult +{ + /// The boolean discriminator. + [JsonPropertyName("handled")] + public bool Handled { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } + /// When true, the runtime will not process subsequent queued commands until a new request comes in. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("stopProcessingQueue")] + public bool? StopProcessingQueue { get; set; } } -/// Permission-decision request variant to approve and persist a permission for a project location, with approval details and location key. -/// The approve-for-location variant of . +/// Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocation : PermissionDecision +internal sealed class CommandsRespondToQueuedCommandRequest { - /// - [JsonIgnore] - public override string Kind => "approve-for-location"; + /// Request ID from the `command.queued` event the host is responding to. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Approval to persist for this location. - [JsonPropertyName("approval")] - public required PermissionDecisionApproveForLocationApproval Approval { get; set; } + /// Result of the queued command execution. + [JsonPropertyName("result")] + public QueuedCommandResult Result { get => field ??= new(); set; } - /// Location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public required string LocationKey { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision request variant to permanently approve a URL domain across sessions. -/// The approve-permanently variant of . +/// Telemetry engagement ID for the session, when available. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApprovePermanently : PermissionDecision +public sealed class SessionTelemetryEngagement { - /// - [JsonIgnore] - public override string Kind => "approve-permanently"; - - /// URL domain to approve permanently. - [JsonPropertyName("domain")] - public required string Domain { get; set; } + /// Current telemetry engagement ID, when available. + [JsonPropertyName("engagementId")] + public string? EngagementId { get; set; } } -/// Permission-decision request variant to reject a pending permission request, with optional feedback. -/// The reject variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionReject : PermissionDecision +internal sealed class SessionTelemetryGetEngagementIdRequest { - /// - [JsonIgnore] - public override string Kind => "reject"; - - /// Optional feedback explaining the rejection. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision variant indicating no user was available to confirm the request. -/// The user-not-available variant of . +/// Feature override key/value pairs to attach to subsequent telemetry events from this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionUserNotAvailable : PermissionDecision +internal sealed class TelemetrySetFeatureOverridesRequest { - /// - [JsonIgnore] - public override string Kind => "user-not-available"; + /// Override key/value pairs to attach to subsequent telemetry events from this session. Replaces any previously-set overrides. + [JsonPropertyName("features")] + public IDictionary Features { get => field ??= new Dictionary(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision variant indicating the request was approved. -/// The approved variant of . +/// Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproved : PermissionDecision +public sealed class UIEphemeralQueryResult { - /// - [JsonIgnore] - public override string Kind => "approved"; + /// Answer returned by the model. + [JsonPropertyName("answer")] + public string Answer { get; set; } = string.Empty; } -/// Permission-decision variant indicating approval was remembered for the session, with approval details. -/// The approved-for-session variant of . +/// Transient question to answer without adding it to conversation history. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApprovedForSession : PermissionDecision +internal sealed class UIEphemeralQueryRequest { - /// - [JsonIgnore] - public override string Kind => "approved-for-session"; + /// Question to answer from the current conversation context. + [JsonPropertyName("question")] + public string Question { get; set; } = string.Empty; - /// The approval to add as a session-scoped rule. - [JsonPropertyName("approval")] - public required UserToolSessionApproval Approval { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Permission-decision variant indicating approval was persisted for a project location, with approval details and location key. -/// The approved-for-location variant of . +/// The elicitation response (accept with form values, decline, or cancel). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApprovedForLocation : PermissionDecision +public sealed class UIElicitationResponse { - /// - [JsonIgnore] - public override string Kind => "approved-for-location"; + /// MCP response metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// The approval to persist for this location. - [JsonPropertyName("approval")] - public required UserToolSessionApproval Approval { get; set; } + /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). + [JsonPropertyName("action")] + public UIElicitationResponseAction Action { get; set; } - /// The location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public required string LocationKey { get; set; } + /// The form values submitted by the user (present when action is 'accept'). + [JsonPropertyName("content")] + public IDictionary? Content { get; set; } } -/// Permission-decision variant indicating the request was cancelled before use, with an optional reason. -/// The cancelled variant of . +/// JSON Schema describing the form fields to present to the user. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionCancelled : PermissionDecision +public sealed class UIElicitationSchema { - /// - [JsonIgnore] - public override string Kind => "cancelled"; + /// Form field definitions, keyed by field name. + [JsonPropertyName("properties")] + public IDictionary Properties { get => field ??= new Dictionary(); set; } - /// Optional explanation of why the request was cancelled. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// List of required field names. + [JsonPropertyName("required")] + public IList? Required { get; set; } + + /// Schema type indicator (always 'object'). + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; } -/// Permission-decision variant indicating explicit denial by permission rules, with the matching rules. -/// The denied-by-rules variant of . +/// Metadata controlling an MCP task's lifetime. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByRules : PermissionDecision +public sealed class McpTaskMetadata { - /// - [JsonIgnore] - public override string Kind => "denied-by-rules"; - - /// Rules that denied the request. - [JsonPropertyName("rules")] - public required IList Rules { get; set; } + /// Task time-to-live. + [JsonPropertyName("ttl")] + public long? Ttl { get; set; } } -/// Permission-decision variant indicating no approval rule matched and user confirmation was unavailable. -/// The denied-no-approval-rule-and-could-not-request-from-user variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "denied-no-approval-rule-and-could-not-request-from-user"; -} - -/// Permission-decision variant indicating the user denied an interactive prompt, with optional feedback and force-reject flag. -/// The denied-interactively-by-user variant of . +/// Prompt message and JSON schema describing the form fields to elicit from the user. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedInteractivelyByUser : PermissionDecision +internal sealed class UIElicitationRequest { - /// - [JsonIgnore] - public override string Kind => "denied-interactively-by-user"; + /// MCP request metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Optional feedback from the user explaining the denial. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } + /// Message describing what information is needed from the user. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// Whether to force-reject the current agent turn. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("forceReject")] - public bool? ForceReject { get; set; } -} + /// Elicitation mode. Omitted and form are equivalent for structured elicitation. + [JsonPropertyName("mode")] + public McpElicitationFormMode? Mode { get; set; } -/// Permission-decision variant indicating denial by content-exclusion policy, with path and message. -/// The denied-by-content-exclusion-policy variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByContentExclusionPolicy : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "denied-by-content-exclusion-policy"; + /// JSON Schema describing the form fields to present to the user. + [JsonPropertyName("requestedSchema")] + public UIElicitationSchema RequestedSchema { get => field ??= new(); set; } - /// Human-readable explanation of why the path was excluded. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// File path that triggered the exclusion. - [JsonPropertyName("path")] - public required string Path { get; set; } + /// MCP task metadata. + [JsonPropertyName("task")] + public McpTaskMetadata? Task { get; set; } } -/// Permission-decision variant indicating denial by a permission request hook, with optional message and interrupt flag. -/// The denied-by-permission-request-hook variant of . +/// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByPermissionRequestHook : PermissionDecision +public sealed class UIElicitationResult { - /// - [JsonIgnore] - public override string Kind => "denied-by-permission-request-hook"; - - /// Whether to interrupt the current agent turn. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("interrupt")] - public bool? Interrupt { get; set; } - - /// Optional message from the hook explaining the denial. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Whether the response was accepted. False if the request was already resolved by another client. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Pending permission request ID and the decision to apply (approve/reject and scope). +/// Pending elicitation request ID and the user's response (accept/decline/cancel + form values). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionDecisionRequest +internal sealed class UIHandlePendingElicitationRequest { - /// Optional informational context describing how and where this response was made. Omit it to preserve legacy behavior without attributing an origin. - [JsonPropertyName("decisionContext")] - public PermissionDecisionContext? DecisionContext { get; set; } - - /// Request ID of the pending permission request. + /// The unique request ID from the elicitation.requested event. [JsonPropertyName("requestId")] public string RequestId { get; set; } = string.Empty; - /// The client's response to the pending permission prompt. + /// The elicitation response (accept with form values, decline, or cancel). [JsonPropertyName("result")] - public PermissionDecision Result { get => field ??= new(); set; } + public UIElicitationResponse Result { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Pending permission prompt reconstructed from event history, with request ID and user-facing prompt details. +/// Indicates whether the pending UI request was resolved by this call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PendingPermissionRequest +public sealed class UIHandlePendingResult { - /// Permission-recovery episode that authorized this request to surface for interactive attention. - [JsonPropertyName("recoveryEpisodeId")] - public string? RecoveryEpisodeId { get; set; } - - /// The user-facing permission prompt details (commands, write, read, mcp, url, memory, custom-tool, path, hook). - [JsonPropertyName("request")] - public PermissionPromptRequest Request { get; set; } = null!; - - /// Unique identifier for the pending permission request. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// List of pending permission requests reconstructed from event history. +/// User response for a pending user-input request, with answer text and whether it was typed freeform. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PendingPermissionRequestList +public sealed class UIUserInputResponse { - /// Pending permission prompts reconstructed from the session's event history. Equivalent to the set of `permission.requested` events that have not yet been followed by a matching `permission.completed` event. Used by clients (e.g. the CLI) to hydrate UI for prompts that were emitted before the client attached to the session. - [JsonPropertyName("items")] - public IList Items { get => field ??= []; set; } + /// The user's answer text. + [JsonPropertyName("answer")] + public string Answer { get; set; } = string.Empty; + + /// True if the user typed a freeform response, false if they selected a presented choice. Used by telemetry to differentiate between free text input and choice selection. + [JsonPropertyName("wasFreeform")] + public bool WasFreeform { get; set; } } -/// No parameters; returns currently-pending permission requests for the session. +/// Request ID of a pending `user_input.requested` event and the user's response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsPendingRequestsRequest +internal sealed class UIHandlePendingUserInputRequest { + /// The unique request ID from the user_input.requested event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// User response for a pending user-input request, with answer text and whether it was typed freeform. + [JsonPropertyName("response")] + public UIUserInputResponse Response { get => field ??= new(); set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetApproveAllResult +public sealed class UIHandlePendingSamplingResponse { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } } -/// Allow-all toggle for tool permission requests, with an optional telemetry source. +/// Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetApproveAllRequest +internal sealed class UIHandlePendingSamplingRequest { - /// Whether to auto-approve all tool permission requests. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// The unique request ID from the sampling.requested event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. + [JsonPropertyName("response")] + public UIHandlePendingSamplingResponse? Response { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. - [JsonPropertyName("source")] - public PermissionsSetApproveAllSource? Source { get; set; } -} - -/// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetModeResult -{ - /// Authoritative permission mode after the mutation. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } - - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } } -/// Permission mode to apply for the session. +/// Request ID of a pending `auto_mode_switch.requested` event and the user's response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetModeRequest +internal sealed class UIHandlePendingAutoModeSwitchRequest { - /// Optional judge model id for assisted mode. When omitted, the session resolves the provider default: `gpt-5.5` for CAPI sessions and the active session model for BYOK sessions. - [JsonPropertyName("assistedApprovalModel")] - public string? AssistedApprovalModel { get; set; } + /// The unique request ID from the auto_mode_switch.requested event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Permission mode to apply. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } + /// User's choice for auto-mode switching: yes (allow this turn), yes_always (allow + persist as setting), or no (decline). + [JsonPropertyName("response")] + public UIAutoModeSwitchResponse Response { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Optional source for permission-mode telemetry. `organization_targeting` is reserved for startup selection after the authenticated account matches an organization targeting policy; SDK callers default to `rpc` and cannot claim targeting provenance. - [JsonPropertyName("source")] - public PermissionModeSource? Source { get; set; } } -/// Current permission mode. +/// The user's selected action for an exhausted session limit. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsGetModeResult +public sealed class UISessionLimitsExhaustedResponse { - /// Current permission mode. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } + /// Action selected by the user. + [JsonPropertyName("action")] + public UISessionLimitsExhaustedResponseAction Action { get; set; } + + /// AI Credits to add to the current max when action is 'add'. + [JsonPropertyName("additionalAiCredits")] + public double? AdditionalAiCredits { get; set; } + + /// New absolute max AI Credits when action is 'set'. + [JsonPropertyName("maxAiCredits")] + public double? MaxAiCredits { get; set; } } -/// No parameters. +/// Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsGetModeRequest +internal sealed class UIHandlePendingSessionLimitsExhaustedRequest { + /// The unique request ID from the session_limits_exhausted.requested event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// The selected session-limit action. + [JsonPropertyName("response")] + public UISessionLimitsExhaustedResponse Response { get => field ??= new(); set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsModifyRulesResult +public sealed class UIExitPlanModeResponse { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Whether the plan was approved. + [JsonPropertyName("approved")] + public bool Approved { get; set; } + + /// Whether subsequent edits should be auto-approved without confirmation. + [JsonPropertyName("autoApproveEdits")] + public bool? AutoApproveEdits { get; set; } + + /// When true, the agent is instructed to end its turn without starting implementation so the client can restore the session model and auto-submit a fresh implementation turn on it. Set only when a distinct plan configuration (a different model, reasoning effort, or context tier) actually ran the planning turn. + [JsonPropertyName("deferImplementation")] + public bool? DeferImplementation { get; set; } + + /// Feedback from the user when they declined the plan or requested changes. + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } + + /// The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. + [JsonPropertyName("selectedAction")] + public UIExitPlanModeAction? SelectedAction { get; set; } } -/// Scope and add/remove instructions for modifying session- or location-scoped permission rules. +/// Request ID of a pending `exit_plan_mode.requested` event and the user's response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsModifyRulesParams +internal sealed class UIHandlePendingExitPlanModeRequest { - /// Rules to add to the scope. Applied before `remove`/`removeAll`. - [JsonPropertyName("add")] - public IList? Add { get; set; } - - /// Specific rules to remove from the scope. Ignored when `removeAll` is true. - [JsonPropertyName("remove")] - public IList? Remove { get; set; } - - /// When true, removes every rule currently in the scope (after any `add` is applied). Useful for clearing the location scope wholesale. - [JsonPropertyName("removeAll")] - public bool? RemoveAll { get; set; } + /// The unique request ID from the exit_plan_mode.requested event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Whether the change applies to ephemeral session-scoped rules (cleared at session end) or to location-scoped rules persisted via the location-permissions config file. - [JsonPropertyName("scope")] - public PermissionsModifyRulesScope Scope { get; set; } + /// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. + [JsonPropertyName("response")] + public UIExitPlanModeResponse Response { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetRequiredResult +public sealed class UIRegisterDirectAutoModeSwitchHandlerResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Opaque handle representing the registration. Pass this same handle to `unregisterDirectAutoModeSwitchHandler` when the in-process handler is no longer active. Multiple registrations are reference-counted; the server bridge will only dispatch auto-mode-switch requests when no handles are active. + [JsonPropertyName("handle")] + public string Handle { get; set; } = string.Empty; } -/// Toggles whether permission prompts should be bridged into session events for this client. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetRequiredRequest +internal sealed class SessionUiRegisterDirectAutoModeSwitchHandlerRequest { - /// Whether the client wants `permission.requested` events bridged from the session-owned permission service. CLI clients that render prompt UI set this to `true` for as long as their listener is mounted; headless callers leave it unset (the default is `false`). - [JsonPropertyName("required")] - public bool Required { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// Indicates whether the handle was active and the registration count was decremented. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsResetSessionApprovalsResult +public sealed class UIUnregisterDirectAutoModeSwitchHandlerResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// True if the handle was active and decremented the counter; false if the handle was unknown. + [JsonPropertyName("unregistered")] + public bool Unregistered { get; set; } } -/// Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. +/// Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsResetSessionApprovalsRequest +internal sealed class UIUnregisterDirectAutoModeSwitchHandlerRequest { - /// Whether location-scoped approvals and exact session-approved paths are cleared too. Defaults to `true`. - [JsonPropertyName("includeLocation")] - public bool? IncludeLocation { get; set; } + /// Handle previously returned by `registerDirectAutoModeSwitchHandler`. + [JsonPropertyName("handle")] + public string Handle { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] @@ -21493,235 +22129,216 @@ internal sealed class PermissionsResetSessionApprovalsRequest /// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsNotifyPromptShownResult +public sealed class PermissionsConfigureResult { /// Whether the operation succeeded. [JsonPropertyName("success")] public bool Success { get; set; } } -/// Notification payload describing the permission prompt that the client just rendered. +/// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPromptShownNotification +public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRuleSource { - /// Human-readable description of the prompt the user is being asked to approve. Used by the runtime to fire the registered `permission_prompt` notification hook (e.g. terminal bell, desktop notification). - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Name of the policy source. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Type of the policy source. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; } -/// Snapshot of the session's recursive directory grants, exact session-approved paths, and primary working directory. +/// Single content-exclusion rule supplied to `session.permissions.configure`, with paths, match conditions, and source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsList +public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRule { - /// All directories currently allowed for tool access on this session. - [JsonPropertyName("directories")] - public IList Directories { get => field ??= []; set; } + /// Conditions of which at least one must match. + [JsonPropertyName("ifAnyMatch")] + public IList? IfAnyMatch { get; set; } - /// The primary working directory for this session. - [JsonPropertyName("primary")] - public string Primary { get; set; } = string.Empty; + /// Conditions none of which may match. + [JsonPropertyName("ifNoneMatch")] + public IList? IfNoneMatch { get; set; } - /// Exact paths approved for this session without recursively allowing their descendants. - [JsonPropertyName("sessionApprovedPaths")] - public IList? SessionApprovedPaths { get; set; } -} + /// Path patterns covered by this rule. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } -/// No parameters; returns the session's recursive directory grants and exact session-approved paths. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsPathsListRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. + [JsonPropertyName("source")] + public PermissionsConfigureAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } } -/// Indicates whether the operation succeeded. +/// Content-exclusion policy supplied to `session.permissions.configure`, with rules, last-updated data, and scope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsPathsAddResult +public sealed class PermissionsConfigureAdditionalContentExclusionPolicy { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Opaque policy update timestamp supplied by the host. + [JsonPropertyName("last_updated_at")] + public JsonElement LastUpdatedAt { get; set; } -/// Directory path to add to the session's allowed directories. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsAddParams -{ - /// 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; + /// Content-exclusion rules to apply. + [JsonPropertyName("rules")] + public IList Rules { get => field ??= []; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. + [JsonPropertyName("scope")] + public PermissionsConfigureAdditionalContentExclusionPolicyScope Scope { get; set; } } -/// Indicates whether the operation succeeded. +/// If specified, replaces the session's path-permission policy. The runtime constructs the appropriate PathManager based on these inputs (rooted at the session's working directory). Omit to leave the current path policy unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsPathsUpdatePrimaryResult +public sealed class PermissionPathsConfig { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// 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; } -/// Directory path to set as the session's new primary working directory. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsUpdatePrimaryParams -{ - /// Directory to set as the new primary working directory for the session's permission policy. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Whether to include the system temp directory in the allowed list (defaults to true). Ignored when `unrestricted` is true. + [JsonPropertyName("includeTempDirectory")] + public bool? IncludeTempDirectory { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// If true, the runtime allows access to all paths without prompting. Equivalent to constructing an UnrestrictedPathManager. + [JsonPropertyName("unrestricted")] + public bool? Unrestricted { get; set; } -/// Indicates whether the supplied path is within the session's allowed directories. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsAllowedCheckResult -{ - /// Whether the path is within the session's allowed directories. - [JsonPropertyName("allowed")] - public bool Allowed { get; set; } + /// Workspace root path (special-cased to be allowed even before the directory exists). Ignored when `unrestricted` is true. + [JsonPropertyName("workspacePath")] + public string? WorkspacePath { get; set; } } -/// Path to evaluate against the session's allowed directories. +/// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsAllowedCheckParams +public sealed class PermissionRulesSet { - /// Path to check against the session's allowed directories. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Rules that auto-approve matching requests. + [JsonPropertyName("approved")] + public IList Approved { get => field ??= []; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Rules that auto-deny matching requests. + [JsonPropertyName("denied")] + public IList Denied { get => field ??= []; set; } } -/// Indicates whether the supplied path is within the session's workspace directory. +/// If specified, replaces the session's URL-permission policy. The runtime constructs a fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsWorkspaceCheckResult +public sealed class PermissionUrlsConfig { - /// Whether the path is within the session workspace directory. - [JsonPropertyName("allowed")] - public bool Allowed { get; set; } + /// Initial list of allowed URL/domain patterns. Patterns may include path components. Ignored when `unrestricted` is true. + [JsonPropertyName("initialAllowed")] + public IList? InitialAllowed { get; set; } + + /// If true, the runtime allows access to all URLs without prompting. Initial allow-list is ignored when this is true. + [JsonPropertyName("unrestricted")] + public bool? Unrestricted { get; set; } } -/// Path to evaluate against the session's workspace (primary) directory. +/// Patch of permission policy fields to apply (omit a field to leave it unchanged). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsWorkspaceCheckParams +internal sealed class PermissionsConfigureParams { - /// Path to check against the session workspace directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// If specified, replaces the host-supplied GitHub Content Exclusion policies on the session (combined with natively-discovered policies when evaluating tool/file access). Omit to leave the current policies unchanged. + [JsonPropertyName("additionalContentExclusionPolicies")] + public IList? AdditionalContentExclusionPolicies { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// If specified, sets whether path/URL read permission requests are auto-approved. Omit to leave the current value unchanged. + [JsonPropertyName("approveAllReadPermissionRequests")] + public bool? ApproveAllReadPermissionRequests { get; set; } -/// Resolved location-permissions key and type. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionLocationResolveResult -{ - /// Location key used in the location-permissions store. - [JsonPropertyName("locationKey")] - public string LocationKey { get; set; } = string.Empty; + /// If specified, sets whether tool permission requests are auto-approved without prompting. Omit to leave the current value unchanged. + [JsonPropertyName("approveAllToolPermissionRequests")] + public bool? ApproveAllToolPermissionRequests { get; set; } - /// Whether the location is a git repo or directory. - [JsonPropertyName("locationType")] - public PermissionLocationType LocationType { get; set; } -} + /// If specified, replaces the session's path-permission policy. The runtime constructs the appropriate PathManager based on these inputs (rooted at the session's working directory). Omit to leave the current path policy unchanged. + [JsonPropertyName("paths")] + public PermissionPathsConfig? Paths { get; set; } + + /// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. + [JsonPropertyName("rules")] + public PermissionRulesSet? Rules { get; set; } -/// Working directory to resolve into a location-permissions key. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationResolveParams -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Working directory whose permission location should be resolved. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; + /// If specified, replaces the session's URL-permission policy. The runtime constructs a fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy unchanged. + [JsonPropertyName("urls")] + public PermissionUrlsConfig? Urls { get; set; } } -/// Summary of persisted location permissions applied to the session. +/// Indicates whether the permission decision was applied; false when the request was already resolved. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionLocationApplyResult +public sealed class PermissionRequestResult { - /// Number of persisted allowed directories added to the live path manager. - [JsonPropertyName("appliedDirectoryCount")] - public long AppliedDirectoryCount { get; set; } - - /// Number of location-scoped rules added to the live permission service. - [JsonPropertyName("appliedRuleCount")] - public long AppliedRuleCount { get; set; } - - /// Location-scoped rules applied to the live permission service. - [JsonPropertyName("appliedRules")] - public IList AppliedRules { get => field ??= []; set; } - - /// Whether a different location was applied since the previous apply call. - [JsonPropertyName("changed")] - public bool Changed { get; set; } - - /// Location key used in the location-permissions store. - [JsonPropertyName("locationKey")] - public string LocationKey { get; set; } = string.Empty; - - /// Whether the location is a git repo or directory. - [JsonPropertyName("locationType")] - public PermissionLocationType LocationType { get; set; } + /// Whether the permission request was handled successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Working directory to load persisted location permissions for. +/// The client's response to the pending permission prompt. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationApplyParams +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(PermissionDecisionApproveOnce), "approve-once")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSession), "approve-for-session")] +[JsonDerivedType(typeof(PermissionDecisionApproveReadOnlyForSession), "approve-read-only-for-session")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocation), "approve-for-location")] +[JsonDerivedType(typeof(PermissionDecisionApprovePermanently), "approve-permanently")] +[JsonDerivedType(typeof(PermissionDecisionReject), "reject")] +[JsonDerivedType(typeof(PermissionDecisionUserNotAvailable), "user-not-available")] +[JsonDerivedType(typeof(PermissionDecisionApproved), "approved")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSession), "approved-for-session")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocation), "approved-for-location")] +[JsonDerivedType(typeof(PermissionDecisionCancelled), "cancelled")] +[JsonDerivedType(typeof(PermissionDecisionDeniedByRules), "denied-by-rules")] +[JsonDerivedType(typeof(PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser), "denied-no-approval-rule-and-could-not-request-from-user")] +[JsonDerivedType(typeof(PermissionDecisionDeniedInteractivelyByUser), "denied-interactively-by-user")] +[JsonDerivedType(typeof(PermissionDecisionDeniedByContentExclusionPolicy), "denied-by-content-exclusion-policy")] +[JsonDerivedType(typeof(PermissionDecisionDeniedByPermissionRequestHook), "denied-by-permission-request-hook")] +public partial class PermissionDecision { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; - - /// Working directory whose persisted location permissions should be applied. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. + +/// Permission-decision request variant to approve only the current permission request. +/// The approve-once variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsLocationsAddToolApprovalResult +public partial class PermissionDecisionApproveOnce : PermissionDecision { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// + [JsonIgnore] + public override string Kind => "approve-once"; + + /// True only when a host surfaced this request to a user who approved it. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("approvedInteractively")] + public bool? ApprovedInteractively { get; set; } } -/// Tool approval to persist and apply. +/// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). /// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsCommands), "commands")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsRead), "read")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsWrite), "write")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsMcp), "mcp")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsMcpSampling), "mcp-sampling")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsMemory), "memory")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsCustomTool), "custom-tool")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionManagement), "extension-management")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsWorkflow), "workflow")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess), "extension-permission-access")] -[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess), "extension-env-access")] -public partial class PermissionsLocationsAddToolApprovalDetails +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalCommands), "commands")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalRead), "read")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalWrite), "write")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalMcp), "mcp")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalMcpSampling), "mcp-sampling")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalMemory), "memory")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalCustomTool), "custom-tool")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalExtensionManagement), "extension-management")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalWorkflow), "workflow")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess), "extension-permission-access")] +[JsonDerivedType(typeof(PermissionDecisionApproveForSessionApprovalExtensionEnvAccess), "extension-env-access")] +public partial class PermissionDecisionApproveForSessionApproval { /// The type discriminator. [JsonPropertyName("kind")] @@ -21729,10 +22346,10 @@ public partial class PermissionsLocationsAddToolApprovalDetails } -/// Location-persisted tool approval details for specific command identifiers. -/// The commands variant of . +/// Session-scoped approval details for specific command identifiers. +/// The commands variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsCommands : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalCommands : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21743,30 +22360,30 @@ public partial class PermissionsLocationsAddToolApprovalDetailsCommands : Permis public required IList CommandIdentifiers { get; set; } } -/// Location-persisted tool approval details for read-only filesystem operations. -/// The read variant of . +/// Session-scoped approval details for read-only filesystem operations. +/// The read variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsRead : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalRead : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] public override string Kind => "read"; } -/// Location-persisted tool approval details for filesystem write operations. -/// The write variant of . +/// Session-scoped approval details for filesystem write operations. +/// The write variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsWrite : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalWrite : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] public override string Kind => "write"; } -/// Location-persisted tool approval details for an MCP server tool, or all tools when `toolName` is null. -/// The mcp variant of . +/// Session-scoped approval details for an MCP server tool, or all tools on the server when `toolName` is null. +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMcp : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalMcp : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21781,10 +22398,10 @@ public partial class PermissionsLocationsAddToolApprovalDetailsMcp : Permissions public string? ToolName { get; set; } } -/// Location-persisted tool approval details for MCP sampling requests from a server. -/// The mcp-sampling variant of . +/// Session-scoped approval details for MCP sampling requests from a server. +/// The mcp-sampling variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMcpSampling : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalMcpSampling : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21795,20 +22412,20 @@ public partial class PermissionsLocationsAddToolApprovalDetailsMcpSampling : Per public required string ServerName { get; set; } } -/// Location-persisted tool approval details for writes to long-term memory. -/// The memory variant of . +/// Session-scoped approval details for writes to long-term memory. +/// The memory variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMemory : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalMemory : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] public override string Kind => "memory"; } -/// Location-persisted tool approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Session-scoped approval details for a custom tool, keyed by tool name. +/// The custom-tool variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsCustomTool : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalCustomTool : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21819,10 +22436,10 @@ public partial class PermissionsLocationsAddToolApprovalDetailsCustomTool : Perm public required string ToolName { get; set; } } -/// Location-persisted tool approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// Session-scoped approval details for extension-management operations, optionally narrowed by operation. +/// The extension-management variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsExtensionManagement : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalExtensionManagement : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21834,10 +22451,10 @@ public partial class PermissionsLocationsAddToolApprovalDetailsExtensionManageme public string? Operation { get; set; } } -/// Location-persisted workflow approval, optionally narrowed by approval key. -/// The workflow variant of . +/// Session-scoped workflow approval, optionally narrowed by approval key. +/// The workflow variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsWorkflow : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalWorkflow : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21849,10 +22466,10 @@ public partial class PermissionsLocationsAddToolApprovalDetailsWorkflow : Permis public string? ApprovalKey { get; set; } } -/// Location-persisted tool approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . +/// Session-scoped approval details for an extension's permission-gated capability access, keyed by extension name. +/// The extension-permission-access variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21863,10 +22480,10 @@ public partial class PermissionsLocationsAddToolApprovalDetailsExtensionPermissi public required string ExtensionName { get; set; } } -/// Location-persisted tool approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. -/// The extension-env-access variant of . +/// Session-scoped approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. +/// The extension-env-access variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess : PermissionsLocationsAddToolApprovalDetails +public partial class PermissionDecisionApproveForSessionApprovalExtensionEnvAccess : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] @@ -21881,4021 +22498,5695 @@ public partial class PermissionsLocationsAddToolApprovalDetailsExtensionEnvAcces public required string ExtensionName { get; set; } } -/// Location-scoped tool approval to persist. +/// Permission-decision request variant to approve for the rest of the session, with optional tool approval or URL domain. +/// The approve-for-session variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationAddToolApprovalParams +public partial class PermissionDecisionApproveForSession : PermissionDecision { - /// Tool approval to persist and apply. + /// + [JsonIgnore] + public override string Kind => "approve-for-session"; + + /// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("approval")] - public PermissionsLocationsAddToolApprovalDetails Approval { get => field ??= new(); set; } + public PermissionDecisionApproveForSessionApproval? Approval { get; set; } - /// Location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public string LocationKey { get; set; } = string.Empty; + /// URL domain to approve for the rest of the session (URL prompts only). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("domain")] + public string? Domain { get; set; } +} - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// Approve file-tool read access to specific directories for this logical session, including continuation or resume. +/// The approve-read-only-for-session variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveReadOnlyForSession : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approve-read-only-for-session"; + + /// Canonical directories covered by the read-only grant. + [JsonPropertyName("directories")] + public required IList Directories { get; set; } } -/// Folder trust check result. +/// Approval to persist for this location. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FolderTrustCheckResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalCommands), "commands")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalRead), "read")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalWrite), "write")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalMcp), "mcp")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalMcpSampling), "mcp-sampling")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalMemory), "memory")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalCustomTool), "custom-tool")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionManagement), "extension-management")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalWorkflow), "workflow")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess), "extension-permission-access")] +[JsonDerivedType(typeof(PermissionDecisionApproveForLocationApprovalExtensionEnvAccess), "extension-env-access")] +public partial class PermissionDecisionApproveForLocationApproval { - /// Whether the folder is trusted. - [JsonPropertyName("trusted")] - public bool Trusted { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Folder path to check for trust. + +/// Location-scoped approval details for specific command identifiers. +/// The commands variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FolderTrustCheckParams +public partial class PermissionDecisionApproveForLocationApprovalCommands : PermissionDecisionApproveForLocationApproval { - /// Folder path to check. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "commands"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Command identifiers covered by this approval. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } } -/// Indicates whether the operation succeeded. +/// Location-scoped approval details for read-only filesystem operations. +/// The read variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsFolderTrustAddTrustedResult +public partial class PermissionDecisionApproveForLocationApprovalRead : PermissionDecisionApproveForLocationApproval { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// + [JsonIgnore] + public override string Kind => "read"; } -/// Folder path to add to trusted folders. +/// Location-scoped approval details for filesystem write operations. +/// The write variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FolderTrustAddParams +public partial class PermissionDecisionApproveForLocationApprovalWrite : PermissionDecisionApproveForLocationApproval { - /// Folder path to mark as trusted. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "write"; } -/// Indicates whether the operation succeeded. +/// Location-scoped approval details for an MCP server tool, or all tools on the server when `toolName` is null. +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsUrlsSetUnrestrictedModeResult +public partial class PermissionDecisionApproveForLocationApprovalMcp : PermissionDecisionApproveForLocationApproval { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// + [JsonIgnore] + public override string Kind => "mcp"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } } -/// Whether the URL-permission policy should run in unrestricted mode. +/// Location-scoped approval details for MCP sampling requests from a server. +/// The mcp-sampling variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionUrlsSetUnrestrictedModeParams +public partial class PermissionDecisionApproveForLocationApprovalMcpSampling : PermissionDecisionApproveForLocationApproval { - /// Whether to allow access to all URLs without prompting. Toggles the runtime's URL-permission policy in place. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// + [JsonIgnore] + public override string Kind => "mcp-sampling"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// The repository the remote session targets. +/// Location-scoped approval details for writes to long-term memory. +/// The memory variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSnapshotRemoteMetadataRepository +public partial class PermissionDecisionApproveForLocationApprovalMemory : PermissionDecisionApproveForLocationApproval { - /// The branch the remote session is operating on. - [JsonPropertyName("branch")] - public string Branch { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "memory"; +} - /// The GitHub repository name (without owner). - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// Location-scoped approval details for a custom tool, keyed by tool name. +/// The custom-tool variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalCustomTool : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "custom-tool"; - /// The GitHub owner (user or organization) of the target repository. - [JsonPropertyName("owner")] - public string Owner { get; set; } = string.Empty; + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } } -/// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. +/// Location-scoped approval details for extension-management operations, optionally narrowed by operation. +/// The extension-management variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSnapshotRemoteMetadata +public partial class PermissionDecisionApproveForLocationApprovalExtensionManagement : PermissionDecisionApproveForLocationApproval { - /// The pull request number the remote session is associated with, if any. - [JsonPropertyName("pullRequestNumber")] - public long? PullRequestNumber { get; set; } + /// + [JsonIgnore] + public override string Kind => "extension-management"; - /// The repository the remote session targets. - [JsonPropertyName("repository")] - public MetadataSnapshotRemoteMetadataRepository Repository { get => field ??= new(); set; } + /// Optional operation identifier; when omitted, the approval covers all extension management operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operation")] + public string? Operation { get; set; } +} - /// The original resource identifier (task ID or PR node ID), preserved across event-replay reconstructions. Falls back to `sessionId` when absent. - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } +/// Location-scoped workflow approval, optionally narrowed by approval key. +/// The workflow variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalWorkflow : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "workflow"; - /// Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. - [JsonPropertyName("taskType")] - public MetadataSnapshotRemoteMetadataTaskType? TaskType { get; set; } + /// Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("approvalKey")] + public string? ApprovalKey { get; set; } } -/// Public-facing projection of workspace metadata for SDK / TUI consumers. -public sealed class SessionMetadataSnapshotWorkspace +/// Location-scoped approval details for an extension's permission-gated capability access, keyed by extension name. +/// The extension-permission-access variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess : PermissionDecisionApproveForLocationApproval { - /// Branch checked out at session start, if any. - [JsonPropertyName("branch")] - public string? Branch { get; set; } + /// + [JsonIgnore] + public override string Kind => "extension-permission-access"; - /// ISO 8601 timestamp when the workspace was created. - [JsonPropertyName("created_at")] - public DateTimeOffset? CreatedAt { get; set; } + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } +} - /// Current working directory at session start. - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } +/// Location-scoped approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. +/// The extension-env-access variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalExtensionEnvAccess : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "extension-env-access"; - /// Resolved git root for cwd, if any. - [JsonPropertyName("git_root")] - public string? GitRoot { get; set; } + /// Names of the sensitive environment variables this approval covers. Values are never persisted. + [JsonPropertyName("environmentVariables")] + public required IList EnvironmentVariables { get; set; } - /// Repository host type, if known. - [JsonPropertyName("host_type")] - public WorkspaceSummaryHostType? HostType { get; set; } + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } +} - /// Workspace identifier (1:1 with sessionId). - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; +/// Permission-decision request variant to approve and persist a permission for a project location, with approval details and location key. +/// The approve-for-location variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocation : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approve-for-location"; - /// Display name for the session, if set. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Approval to persist for this location. + [JsonPropertyName("approval")] + public required PermissionDecisionApproveForLocationApproval Approval { get; set; } - /// Repository identifier in 'owner/repo' or 'org/project/repo' format, if any. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// Location key (git root or cwd) to persist the approval to. + [JsonPropertyName("locationKey")] + public required string LocationKey { get; set; } +} - /// ISO 8601 timestamp when the workspace was last updated. - [JsonPropertyName("updated_at")] - public DateTimeOffset? UpdatedAt { get; set; } +/// Permission-decision request variant to permanently approve a URL domain across sessions. +/// The approve-permanently variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApprovePermanently : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approve-permanently"; - /// Whether the display name was explicitly set by the user. - [JsonPropertyName("user_named")] - public bool? UserNamed { get; set; } + /// URL domain to approve permanently. + [JsonPropertyName("domain")] + public required string Domain { get; set; } } -/// Point-in-time snapshot of slow-changing session identifier and state fields. +/// Permission-decision request variant to reject a pending permission request, with optional feedback. +/// The reject variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionMetadataSnapshot +public partial class PermissionDecisionReject : PermissionDecision { - /// True when the session was detected to be in use by another process at construction time. Local consumers may surface a confirmation prompt before fully attaching. Always false for new sessions. - [JsonPropertyName("alreadyInUse")] - public bool AlreadyInUse { get; set; } - - /// Runtime client name associated with the session (telemetry identifier). - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// + [JsonIgnore] + public override string Kind => "reject"; - /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot'). - [JsonPropertyName("currentMode")] - public MetadataSnapshotCurrentMode CurrentMode { get; set; } + /// Optional feedback explaining the rejection. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } +} - /// Live indexed-search state for this session activation. Omitted by runtimes that do not expose indexed-search status; absence does not indicate enablement. - [JsonPropertyName("indexedSearch")] - public IndexedSearchState? IndexedSearch { get; set; } +/// Permission-decision variant indicating no user was available to confirm the request. +/// The user-not-available variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionUserNotAvailable : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "user-not-available"; +} - /// User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. - [JsonPropertyName("initialName")] - public string? InitialName { get; set; } +/// Permission-decision variant indicating the request was approved. +/// The approved variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproved : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approved"; +} - /// Whether this is a remote session (i.e., one whose runtime executes elsewhere and is steered through this process). - [JsonPropertyName("isRemote")] - public bool IsRemote { get; set; } - - /// ISO 8601 timestamp of when the session's persisted state was last modified on disk. For new sessions, equals startTime. For resumed sessions, reflects the previous modification time at construction. - [JsonPropertyName("modifiedTime")] - public DateTimeOffset ModifiedTime { get; set; } - - /// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. - [JsonPropertyName("remoteMetadata")] - public MetadataSnapshotRemoteMetadata? RemoteMetadata { get; set; } +/// Permission-decision variant indicating approval was remembered for the session, with approval details. +/// The approved-for-session variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApprovedForSession : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approved-for-session"; - /// Currently selected model identifier, if any. - [JsonPropertyName("selectedModel")] - public string? SelectedModel { get; set; } + /// The approval to add as a session-scoped rule. + [JsonPropertyName("approval")] + public required UserToolSessionApproval Approval { get; set; } +} - /// The unique identifier of the session. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// Permission-decision variant indicating approval was persisted for a project location, with approval details and location key. +/// The approved-for-location variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApprovedForLocation : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approved-for-location"; - /// Current session limits, or null when no limits are active. - [JsonPropertyName("sessionLimits")] - public SessionLimitsConfig? SessionLimits { get; set; } + /// The approval to persist for this location. + [JsonPropertyName("approval")] + public required UserToolSessionApproval Approval { get; set; } - /// ISO 8601 timestamp of when the session started. - [JsonPropertyName("startTime")] - public DateTimeOffset StartTime { get; set; } + /// The location key (git root or cwd) to persist the approval to. + [JsonPropertyName("locationKey")] + public required string LocationKey { get; set; } +} - /// Short human-readable summary of the session, if known. Omitted when no summary has been generated. - [JsonPropertyName("summary")] - public string? Summary { get; set; } +/// Permission-decision variant indicating the request was cancelled before use, with an optional reason. +/// The cancelled variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionCancelled : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "cancelled"; - /// Absolute path to the session's current working directory. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; + /// Optional explanation of why the request was cancelled. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reason")] + public string? Reason { get; set; } +} - /// Public-facing workspace metadata for this session, or null if the session has no associated workspace. Excludes runtime-internal fields (GitHub IDs, summary count, internal flags). - [JsonPropertyName("workspace")] - public SessionMetadataSnapshotWorkspace? Workspace { get; set; } +/// Permission-decision variant indicating explicit denial by permission rules, with the matching rules. +/// The denied-by-rules variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionDeniedByRules : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "denied-by-rules"; - /// Absolute path to the session's workspace directory on disk, or null if the session has no associated workspace. - [JsonPropertyName("workspacePath")] - public string? WorkspacePath { get; set; } + /// Rules that denied the request. + [JsonPropertyName("rules")] + public required IList Rules { get; set; } } -/// Identifies the target session. +/// Permission-decision variant indicating no approval rule matched and user confirmation was unavailable. +/// The denied-no-approval-rule-and-could-not-request-from-user variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataSnapshotRequest +public partial class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser : PermissionDecision { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "denied-no-approval-rule-and-could-not-request-from-user"; } -/// Identifies the target session. +/// Permission-decision variant indicating the user denied an interactive prompt, with optional feedback and force-reject flag. +/// The denied-interactively-by-user variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataGetClientMetadataRequest +public partial class PermissionDecisionDeniedInteractivelyByUser : PermissionDecision { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "denied-interactively-by-user"; + + /// Optional feedback from the user explaining the denial. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } + + /// Whether to force-reject the current agent turn. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("forceReject")] + public bool? ForceReject { get; set; } } -/// Atomic patch for client-owned session metadata. Operations apply in clear, remove, then set order. The resulting bag must satisfy the ClientMetadata entry and serialized-size limits. Local storage coordinates concurrent runtime processes; custom SessionFs providers must serialize writers that access the same session from multiple processes. +/// Permission-decision variant indicating denial by content-exclusion policy, with path and message. +/// The denied-by-content-exclusion-policy variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataUpdateClientMetadataRequest +public partial class PermissionDecisionDeniedByContentExclusionPolicy : PermissionDecision { - /// Remove every existing client metadata entry before applying remove and set. Defaults to false. - [JsonPropertyName("clear")] - public bool? Clear { get; set; } - - /// Case-sensitive keys to remove. Missing keys are ignored. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. - [JsonPropertyName("remove")] - public IList? Remove { get; set; } + /// + [JsonIgnore] + public override string Kind => "denied-by-content-exclusion-policy"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Human-readable explanation of why the path was excluded. + [JsonPropertyName("message")] + public required string Message { get; set; } - /// String entries to add or replace. Set wins when a key also appears in remove. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. Each value may contain at most 16 KiB of UTF-8 data. - [JsonPropertyName("set")] - public IDictionary? Set { get; set; } + /// File path that triggered the exclusion. + [JsonPropertyName("path")] + public required string Path { get; set; } } -/// Indicates whether the local session is currently processing a turn or background continuation. +/// Permission-decision variant indicating denial by a permission request hook, with optional message and interrupt flag. +/// The denied-by-permission-request-hook variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataIsProcessingResult +public partial class PermissionDecisionDeniedByPermissionRequestHook : PermissionDecision { - /// Whether the session is currently processing user/agent messages. False for non-local sessions (which don't run a local agentic loop). Reflects an in-flight turn or background continuation. - [JsonPropertyName("processing")] - public bool Processing { get; set; } + /// + [JsonIgnore] + public override string Kind => "denied-by-permission-request-hook"; + + /// Whether to interrupt the current agent turn. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interrupt")] + public bool? Interrupt { get; set; } + + /// Optional message from the hook explaining the denial. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } } -/// Identifies the target session. +/// Pending permission request ID and the decision to apply (approve/reject and scope). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataIsProcessingRequest +internal sealed class PermissionDecisionRequest { + /// Optional informational context describing how and where this response was made. Omit it to preserve legacy behavior without attributing an origin. + [JsonPropertyName("decisionContext")] + public PermissionDecisionContext? DecisionContext { get; set; } + + /// Request ID of the pending permission request. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// The client's response to the pending permission prompt. + [JsonPropertyName("result")] + public PermissionDecision Result { get => field ??= new(); set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Current activity flags for the session. +/// Pending permission prompt reconstructed from event history, with request ID and user-facing prompt details. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionActivity +public sealed class PendingPermissionRequest { - /// Whether an in-flight operation can currently be aborted. - [JsonPropertyName("abortable")] - public bool Abortable { get; set; } + /// Permission-recovery episode that authorized this request to surface for interactive attention. + [JsonPropertyName("recoveryEpisodeId")] + public string? RecoveryEpisodeId { get; set; } - /// Whether the session currently has active work, including running turns or tasks. - [JsonPropertyName("hasActiveWork")] - public bool HasActiveWork { get; set; } + /// The user-facing permission prompt details (commands, write, read, mcp, url, memory, custom-tool, path, hook). + [JsonPropertyName("request")] + public PermissionPromptRequest Request { get; set; } = null!; + + /// Unique identifier for the pending permission request. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; } -/// Identifies the target session. +/// List of pending permission requests reconstructed from event history. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataActivityRequest +public sealed class PendingPermissionRequestList +{ + /// Pending permission prompts reconstructed from the session's event history. Equivalent to the set of `permission.requested` events that have not yet been followed by a matching `permission.completed` event. Used by clients (e.g. the CLI) to hydrate UI for prompts that were emitted before the client attached to the session. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } +} + +/// No parameters; returns currently-pending permission requests for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsPendingRequestsRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Token-usage breakdown for the session's current context window. -public sealed class MetadataContextInfoResultContextInfo +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsSetApproveAllResult { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%). - [JsonPropertyName("bufferTokens")] - public long BufferTokens { get; set; } - - /// Token count at which background compaction starts (configurable percentage of promptTokenLimit). - [JsonPropertyName("compactionThreshold")] - public long CompactionThreshold { get; set; } - - /// Tokens consumed by user/assistant/tool messages. - [JsonPropertyName("conversationTokens")] - public long ConversationTokens { get; set; } - - /// Prompt token limit plus the model's full output token limit. - [JsonPropertyName("limit")] - public long Limit { get; set; } - - /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools). - [JsonPropertyName("mcpToolsTokens")] - public long McpToolsTokens { get; set; } - - /// The model used for token counting. - [JsonPropertyName("modelName")] - public string ModelName { get; set; } = string.Empty; - - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified). - [JsonPropertyName("promptTokenLimit")] - public long PromptTokenLimit { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Tokens consumed by the system prompt. - [JsonPropertyName("systemTokens")] - public long SystemTokens { get; set; } +/// Allow-all toggle for tool permission requests, with an optional telemetry source. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsSetApproveAllRequest +{ + /// Whether to auto-approve all tool permission requests. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Tokens consumed by tool definitions sent to the model (excludes deferred tools). - [JsonPropertyName("toolDefinitionsTokens")] - public long ToolDefinitionsTokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Sum of system, conversation and tool-definition tokens. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. + [JsonPropertyName("source")] + public PermissionsSetApproveAllSource? Source { get; set; } } -/// Token breakdown for the session's current context window, or null if uninitialized. +/// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextInfoResult +public sealed class PermissionsSetModeResult { - /// Token breakdown for the current context window, or null if the session has not yet been initialized (no system prompt or tool metadata cached). - [JsonPropertyName("contextInfo")] - public MetadataContextInfoResultContextInfo? ContextInfo { get; set; } + /// Authoritative permission mode after the mutation. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } + + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Model identifier and token limits used to compute the context-info breakdown. +/// Permission mode to apply for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataContextInfoRequest +internal sealed class PermissionsSetModeRequest { - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. - [JsonPropertyName("outputTokenLimit")] - public long OutputTokenLimit { get; set; } - - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. - [JsonPropertyName("promptTokenLimit")] - public long PromptTokenLimit { get; set; } + /// Optional judge model id for assisted mode. When omitted, the session resolves the provider default: `gpt-5.5` for CAPI sessions and the active session model for BYOK sessions. + [JsonPropertyName("assistedApprovalModel")] + public string? AssistedApprovalModel { get; set; } - /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. - [JsonPropertyName("selectedModel")] - public string? SelectedModel { get; set; } + /// Permission mode to apply. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Optional source for permission-mode telemetry. `organization_targeting` is reserved for startup selection after the authenticated account matches an organization targeting policy; SDK callers default to `rpc` and cannot claim targeting provenance. + [JsonPropertyName("source")] + public PermissionModeSource? Source { get; set; } } -/// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. -public sealed class MetadataContextAttributionResultContextAttributionCategories +/// Current permission mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsGetModeResult { - /// Output reserve plus post-blocking-threshold buffer. - [JsonPropertyName("buffer")] - public long Buffer { get; set; } - - /// Custom-instructions tokens (0 when none are configured). - [JsonPropertyName("customInstructions")] - public long CustomInstructions { get; set; } - - /// Remaining unused window capacity (clamped at 0). - [JsonPropertyName("freeSpace")] - public long FreeSpace { get; set; } - - /// MCP tool-definition tokens. - [JsonPropertyName("mcpTools")] - public long McpTools { get; set; } - - /// Conversation (user/assistant/tool) message tokens. - [JsonPropertyName("messages")] - public long Messages { get; set; } - - /// System prompt tokens, excluding custom instructions. - [JsonPropertyName("systemPrompt")] - public long SystemPrompt { get; set; } - - /// Non-MCP tool-definition tokens. - [JsonPropertyName("systemTools")] - public long SystemTools { get; set; } + /// Current permission mode. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } } -/// Successful compaction history for the session. -public sealed class MetadataContextAttributionResultContextAttributionCompactions +/// No parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsGetModeRequest { - /// Number of successful compactions in this session. - [JsonPropertyName("count")] - public long Count { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// RPC data type for MetadataContextAttributionResultContextAttributionEntry operations. -public sealed class MetadataContextAttributionResultContextAttributionEntry +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsModifyRulesResult { - /// Supplementary per-entry metadata (e.g. `messageCount`, `role`, `evictable`, `pluginSource`). Values are stringified; parse as needed and ignore unrecognized keys. - [JsonPropertyName("attributes")] - public IDictionary? Attributes { get; set; } - - /// Identifier for this entry, formed by joining its `kind` and source name (e.g. `tool:bash`, `skill:tmux`, `toolDefinition:bash`); unique within the snapshot. Use it to match the same entry across snapshots, to correlate with other APIs (skill/agent/MCP registries), and as the `parentId` target for nesting. Distinct from the human-facing `label`. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Source category for this entry. Not a closed set — tolerate unknown values. Known values today: `skill`, `subagent`, `mcpServer`, `tool`, `system`, `toolDefinition`, `plugin`. - [JsonPropertyName("kind")] - public string Kind { get; set; } = string.Empty; - - /// Human-readable display label, e.g. `bash` or `skill: tmux`. Presentation-only; may be localized/reformatted without notice — do not key off it. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; - - /// Optional `id` of the parent entry: e.g. a `plugin` entry parenting its `skill`/`mcpServer` entries, or the `system` entry parenting `toolDefinition` entries. Omitted for top-level entries. - [JsonPropertyName("parentId")] - public string? ParentId { get; set; } - - /// Token count currently in context attributable to this entry. - [JsonPropertyName("tokens")] - public long Tokens { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. -public sealed class MetadataContextAttributionResultContextAttribution +/// Scope and add/remove instructions for modifying session- or location-scoped permission rules. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsModifyRulesParams { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. - [JsonPropertyName("bufferTokens")] - public long BufferTokens { get; set; } - - /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. - [JsonPropertyName("categories")] - public MetadataContextAttributionResultContextAttributionCategories Categories { get => field ??= new(); set; } - - /// Successful compaction history for the session. - [JsonPropertyName("compactions")] - public MetadataContextAttributionResultContextAttributionCompactions Compactions { get => field ??= new(); set; } - - /// Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. - [JsonPropertyName("compactionThreshold")] - public long CompactionThreshold { get; set; } - - /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } - - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. - [JsonPropertyName("limit")] - public long Limit { get; set; } + /// Rules to add to the scope. Applied before `remove`/`removeAll`. + [JsonPropertyName("add")] + public IList? Add { get; set; } - /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. - [JsonPropertyName("modelId")] - public string ModelId { get; set; } = string.Empty; + /// Specific rules to remove from the scope. Ignored when `removeAll` is true. + [JsonPropertyName("remove")] + public IList? Remove { get; set; } - /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). - [JsonPropertyName("modelSource")] - public string ModelSource { get; set; } = string.Empty; + /// When true, removes every rule currently in the scope (after any `add` is applied). Useful for clearing the location scope wholesale. + [JsonPropertyName("removeAll")] + public bool? RemoveAll { get; set; } - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. - [JsonPropertyName("promptTokenLimit")] - public long PromptTokenLimit { get; set; } + /// Whether the change applies to ephemeral session-scoped rules (cleared at session end) or to location-scoped rules persisted via the location-permissions config file. + [JsonPropertyName("scope")] + public PermissionsModifyRulesScope Scope { get; set; } - /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Per-source attribution breakdown for the session's current context window, or null if uninitialized. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextAttributionResult +public sealed class PermissionsSetRequiredResult { - /// Per-source context-window attribution, or null if the session has not yet been initialized (no system prompt or tool metadata cached). - [JsonPropertyName("contextAttribution")] - public MetadataContextAttributionResultContextAttribution? ContextAttribution { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Identifies the target session. +/// Toggles whether permission prompts should be bridged into session events for this client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataGetContextAttributionRequest +internal sealed class PermissionsSetRequiredRequest { + /// Whether the client wants `permission.requested` events bridged from the session-owned permission service. CLI clients that render prompt UI set this to `true` for as long as their listener is mounted; headless callers leave it unset (the default is `false`). + [JsonPropertyName("required")] + public bool Required { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single large message currently in context. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContextHeaviestMessage +public sealed class PermissionsResetSessionApprovalsResult { - /// Stable identifier for this message within the snapshot. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Human-readable source label, e.g. `tool: bash` or `skill: tmux`. Presentation-only. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Role of the chat message (`user`, `assistant`, or `tool`). - [JsonPropertyName("role")] - public string Role { get; set; } = string.Empty; +/// Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PermissionsResetSessionApprovalsRequest +{ + /// Whether location-scoped approvals and exact session-approved paths are cleared too. Defaults to `true`. + [JsonPropertyName("includeLocation")] + public bool? IncludeLocation { get; set; } - /// Token count currently in context for this individual message. - [JsonPropertyName("tokens")] - public long Tokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The heaviest individual messages in the session's context window, most-expensive first. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextHeaviestMessagesResult +public sealed class PermissionsNotifyPromptShownResult { - /// Heaviest messages, most-expensive first. - [JsonPropertyName("messages")] - public IList Messages { get => field ??= []; set; } - - /// Total token count of the current context window, so callers can compute each message's share without a second call. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Parameters for the heaviest-messages query. +/// Notification payload describing the permission prompt that the client just rendered. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataContextHeaviestMessagesRequest +internal sealed class PermissionPromptShownNotification { - /// Maximum number of messages to return, most-expensive first. Omit for the server default. - [JsonPropertyName("limit")] - public long? Limit { get; set; } + /// Human-readable description of the prompt the user is being asked to approve. Used by the runtime to fire the registered `permission_prompt` notification hook (e.g. terminal bell, desktop notification). + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event); move a local session's working directory via `metadata.setWorkingDirectory` instead. +/// Snapshot of the session's recursive directory grants, exact session-approved paths, and primary working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataRecordContextChangeResult +public sealed class PermissionPathsList { -} + /// All directories currently allowed for tool access on this session. + [JsonPropertyName("directories")] + public IList Directories { get => field ??= []; set; } -/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionWorkingDirectoryContext -{ - /// Merge-base commit SHA (fork point from the remote default branch). - [JsonPropertyName("baseCommit")] - public string? BaseCommit { get; set; } - - /// Current git branch name. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// Current working directory path. - [JsonPropertyName("cwd")] - public string Cwd { get; set; } = string.Empty; - - /// Root directory of the git repository, resolved via git rev-parse. - [JsonPropertyName("gitRoot")] - public string? GitRoot { get; set; } - - /// Head commit of the current git branch. - [JsonPropertyName("headCommit")] - public string? HeadCommit { get; set; } - - /// Hosting platform type of the repository. - [JsonPropertyName("hostType")] - public SessionWorkingDirectoryContextHostType? HostType { get; set; } - - /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// The primary working directory for this session. + [JsonPropertyName("primary")] + public string Primary { get; set; } = string.Empty; - /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com"). - [JsonPropertyName("repositoryHost")] - public string? RepositoryHost { get; set; } + /// Exact paths approved for this session without recursively allowing their descendants. + [JsonPropertyName("sessionApprovedPaths")] + public IList? SessionApprovedPaths { get; set; } } -/// Updated working-directory/git context to record on the session. +/// No parameters; returns the session's recursive directory grants and exact session-approved paths. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataRecordContextChangeRequest +internal sealed class PermissionsPathsListRequest { - /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. - [JsonPropertyName("context")] - public SessionWorkingDirectoryContext Context { get => field ??= new(); set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSetWorkingDirectoryResult +public sealed class PermissionsPathsAddResult { - /// Working directory after the update. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. +/// Directory path to add to the session's allowed directories. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataSetWorkingDirectoryRequest +internal sealed class PermissionPathsAddParams { + /// 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; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Absolute path to set as the session's working directory. The runtime updates the session's recorded cwd so subsequent operations (shell tools, file lookups, telemetry) anchor to it. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; } -/// Re-tokenize the session's existing messages against `modelId` and return the token totals. Useful for hosts that want an initial estimate of context usage on session resume, before the next agent turn fires `session.context_info_changed` events. Returns zeros for an empty session. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataRecomputeContextTokensResult +public sealed class PermissionsPathsUpdatePrimaryResult { - /// Tokens contributed by user/assistant/tool messages (excludes system/developer prompts). - [JsonPropertyName("messagesTokenCount")] - public long MessagesTokenCount { get; set; } - - /// Tokens contributed by system/developer prompt snapshots. - [JsonPropertyName("systemTokenCount")] - public long SystemTokenCount { get; set; } - - /// Sum of tokens across chat-context and system-context messages currently held by the session. - [JsonPropertyName("totalTokens")] - public long TotalTokens { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Model identifier to use when re-tokenizing the session's existing messages. +/// Directory path to set as the session's new primary working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataRecomputeContextTokensRequest +internal sealed class PermissionPathsUpdatePrimaryParams { - /// Model identifier used for tokenization. The runtime token-counts both chat-context and system-context messages against this model. - [JsonPropertyName("modelId")] - public string ModelId { get; set; } = string.Empty; + /// Directory to set as the new primary working directory for the session's permission policy. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Availability of built-in job tools surfaced to boundary consumers. +/// Indicates whether the supplied path is within the session's allowed directories. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsBuiltInToolAvailabilitySnapshot +public sealed class PermissionPathsAllowedCheckResult { - /// Whether the create-pull-request tool is available. - [JsonPropertyName("createPullRequest")] - public bool? CreatePullRequest { get; set; } - - /// Whether the report-progress tool is available. - [JsonPropertyName("reportProgress")] - public bool? ReportProgress { get; set; } + /// Whether the path is within the session's allowed directories. + [JsonPropertyName("allowed")] + public bool Allowed { get; set; } } -/// Redacted job settings for a session. The job nonce is excluded. +/// Path to evaluate against the session's allowed directories. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsJobSnapshot +internal sealed class PermissionPathsAllowedCheckParams { - /// Availability of job-specific built-in tools. - [JsonPropertyName("builtInToolAvailability")] - public SessionSettingsBuiltInToolAvailabilitySnapshot? BuiltInToolAvailability { get; set; } - - /// GitHub Actions event type for the job. - [JsonPropertyName("eventType")] - public string? EventType { get; set; } + /// Path to check against the session's allowed directories. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Whether this is the workflow's trigger job. - [JsonPropertyName("isTriggerJob")] - public bool? IsTriggerJob { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Redacted model routing settings for a session. +/// Indicates whether the supplied path is within the session's workspace directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsModelSnapshot +public sealed class PermissionPathsWorkspaceCheckResult { - /// Agent service callback URL for job and progress updates. - [JsonPropertyName("callbackUrl")] - public string? CallbackUrl { get; set; } - - /// Default reasoning effort for the selected model. - [JsonPropertyName("defaultReasoningEffort")] - public string? DefaultReasoningEffort { get; set; } - - /// Agent job identifier for the session. - [JsonPropertyName("instanceId")] - public string? InstanceId { get; set; } - - /// Selected model identifier. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Whether the path is within the session workspace directory. + [JsonPropertyName("allowed")] + public bool Allowed { get; set; } } -/// Online-evaluation settings safe to expose across the SDK boundary. +/// Path to evaluate against the session's workspace (primary) directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsOnlineEvaluationSnapshot +internal sealed class PermissionPathsWorkspaceCheckParams { - /// Whether online evaluation is disabled. - [JsonPropertyName("disableOnlineEvaluation")] - public bool? DisableOnlineEvaluation { get; set; } + /// Path to check against the session workspace directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Whether online-evaluation output-file generation is enabled. - [JsonPropertyName("enableOnlineEvaluationOutputFile")] - public bool? EnableOnlineEvaluationOutputFile { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Redacted repository and GitHub host settings for a session. +/// Resolved location-permissions key and type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsRepoSnapshot +public sealed class PermissionLocationResolveResult { - /// Checked-out repository branch. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// Checked-out commit SHA. - [JsonPropertyName("commit")] - public string? Commit { get; set; } - - /// GitHub server host name. - [JsonPropertyName("host")] - public string? Host { get; set; } - - /// Protocol used to access the GitHub host. - [JsonPropertyName("hostProtocol")] - public string? HostProtocol { get; set; } - - /// GitHub repository database ID. - [JsonPropertyName("id")] - public double? Id { get; set; } - - /// Repository name. - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// GitHub repository owner database ID. - [JsonPropertyName("ownerId")] - public double? OwnerId { get; set; } - - /// Repository owner login. - [JsonPropertyName("ownerName")] - public string? OwnerName { get; set; } - - /// Number of commits in the pull request. - [JsonPropertyName("prCommitCount")] - public double? PrCommitCount { get; set; } - - /// Whether the repository is writable. - [JsonPropertyName("readWrite")] - public bool? ReadWrite { get; set; } - - /// GitHub secret-scanning service URL. - [JsonPropertyName("secretScanningUrl")] - public string? SecretScanningUrl { get; set; } + /// Location key used in the location-permissions store. + [JsonPropertyName("locationKey")] + public string LocationKey { get; set; } = string.Empty; - /// GitHub server base URL. - [JsonPropertyName("serverUrl")] - public string? ServerUrl { get; set; } + /// Whether the location is a git repo or directory. + [JsonPropertyName("locationType")] + public PermissionLocationType LocationType { get; set; } } -/// Redacted validation and memory-tool settings for a session. +/// Working directory to resolve into a location-permissions key. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsValidationSnapshot +internal sealed class PermissionLocationResolveParams { - /// Whether advisory validation is enabled. - [JsonPropertyName("advisoryEnabled")] - public bool? AdvisoryEnabled { get; set; } - - /// Whether CodeQL validation is enabled. - [JsonPropertyName("codeqlEnabled")] - public bool? CodeqlEnabled { get; set; } - - /// Whether code-review validation is enabled. - [JsonPropertyName("codeReviewEnabled")] - public bool? CodeReviewEnabled { get; set; } - - /// Model used for code-review validation. - [JsonPropertyName("codeReviewModel")] - public string? CodeReviewModel { get; set; } - - /// Dependabot validation timeout budget in seconds. - [JsonPropertyName("dependabotTimeout")] - public double? DependabotTimeout { get; set; } - - /// Whether the memory-store tool is enabled. - [JsonPropertyName("memoryStoreEnabled")] - public bool? MemoryStoreEnabled { get; set; } - - /// Whether the memory-vote tool is enabled. - [JsonPropertyName("memoryVoteEnabled")] - public bool? MemoryVoteEnabled { get; set; } - - /// Whether secret-scanning validation is enabled. - [JsonPropertyName("secretScanningEnabled")] - public bool? SecretScanningEnabled { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// General validation timeout budget in seconds. - [JsonPropertyName("timeout")] - public double? Timeout { get; set; } + /// Working directory whose permission location should be resolved. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; } -/// Redacted, serializable view of session runtime settings for SDK boundary consumers. Secrets and raw feature flags are intentionally excluded. +/// Summary of persisted location permissions applied to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsSnapshot +public sealed class PermissionLocationApplyResult { - /// Name of the SDK client that created the session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } - - /// Redacted job settings. - [JsonPropertyName("job")] - public SessionSettingsJobSnapshot Job { get => field ??= new(); set; } - - /// Redacted model routing settings. - [JsonPropertyName("model")] - public SessionSettingsModelSnapshot Model { get => field ??= new(); set; } - - /// Online-evaluation settings safe for SDK consumers. - [JsonPropertyName("onlineEvaluation")] - public SessionSettingsOnlineEvaluationSnapshot OnlineEvaluation { get => field ??= new(); set; } + /// Number of persisted allowed directories added to the live path manager. + [JsonPropertyName("appliedDirectoryCount")] + public long AppliedDirectoryCount { get; set; } - /// Redacted repository and host settings. - [JsonPropertyName("repo")] - public SessionSettingsRepoSnapshot Repo { get => field ??= new(); set; } + /// Number of location-scoped rules added to the live permission service. + [JsonPropertyName("appliedRuleCount")] + public long AppliedRuleCount { get; set; } - /// Session start time as Unix epoch milliseconds. - [JsonPropertyName("startTimeMs")] - public double? StartTimeMs { get; set; } + /// Location-scoped rules applied to the live permission service. + [JsonPropertyName("appliedRules")] + public IList AppliedRules { get => field ??= []; set; } - /// Session timeout in milliseconds. - [JsonPropertyName("timeoutMs")] - public double? TimeoutMs { get; set; } + /// Whether a different location was applied since the previous apply call. + [JsonPropertyName("changed")] + public bool Changed { get; set; } - /// Redacted validation and memory-tool settings. - [JsonPropertyName("validation")] - public SessionSettingsValidationSnapshot Validation { get => field ??= new(); set; } + /// Location key used in the location-permissions store. + [JsonPropertyName("locationKey")] + public string LocationKey { get; set; } = string.Empty; - /// Agent runtime version selector copied from the session settings, such as `latest` or a runtime release identifier. - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Whether the location is a git repo or directory. + [JsonPropertyName("locationType")] + public PermissionLocationType LocationType { get; set; } } -/// Identifies the target session. +/// Working directory to load persisted location permissions for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsSnapshotRequest +internal sealed class PermissionLocationApplyParams { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Working directory whose persisted location permissions should be applied. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; } -/// Result of evaluating a Rust-owned settings predicate. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsEvaluatePredicateResult +public sealed class PermissionsLocationsAddToolApprovalResult { - /// Whether the named settings predicate evaluated to enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Named Rust-owned settings predicate to evaluate for this session. +/// Tool approval to persist and apply. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsEvaluatePredicateRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsCommands), "commands")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsRead), "read")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsWrite), "write")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsMcp), "mcp")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsMcpSampling), "mcp-sampling")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsMemory), "memory")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsCustomTool), "custom-tool")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionManagement), "extension-management")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsWorkflow), "workflow")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess), "extension-permission-access")] +[JsonDerivedType(typeof(PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess), "extension-env-access")] +public partial class PermissionsLocationsAddToolApprovalDetails { - /// Predicate name. The runtime owns the raw feature-flag names and composition logic. - [JsonPropertyName("name")] - public SessionSettingsPredicateName Name { get; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; - - /// Tool name for tool-scoped predicates such as trivial-change handling. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Content-exclusion decision for one requested path. + +/// Location-persisted tool approval details for specific command identifiers. +/// The commands variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContentExclusionPathCheck +public partial class PermissionsLocationsAddToolApprovalDetailsCommands : PermissionsLocationsAddToolApprovalDetails { - /// Whether the session's complete content-exclusion policy excludes the path. - [JsonPropertyName("excluded")] - public bool Excluded { get; set; } + /// + [JsonIgnore] + public override string Kind => "commands"; - /// The path supplied by the caller. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Command identifiers covered by this approval. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } } -/// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. +/// Location-persisted tool approval details for read-only filesystem operations. +/// The read variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContentExclusionCheckPathsResult +public partial class PermissionsLocationsAddToolApprovalDetailsRead : PermissionsLocationsAddToolApprovalDetails { - /// Whether the session's policy service was available for the complete batch. When false, checks is empty and callers must treat every requested path as excluded. - [JsonPropertyName("available")] - public bool Available { get; set; } - - /// Per-path decisions in request order. Empty when available is false. - [JsonPropertyName("checks")] - public IList Checks { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "read"; } -/// Local file system absolute paths within the session working directory to check against its content-exclusion policy. +/// Location-persisted tool approval details for filesystem write operations. +/// The write variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ContentExclusionCheckPathsRequest +public partial class PermissionsLocationsAddToolApprovalDetailsWrite : PermissionsLocationsAddToolApprovalDetails { - /// Local file system absolute paths within the session working directory to check. Results are returned in the same order, including duplicates. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "write"; } -/// Identifier of the spawned process, used to correlate streamed output and exit notifications. +/// Location-persisted tool approval details for an MCP server tool, or all tools when `toolName` is null. +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellExecResult +public partial class PermissionsLocationsAddToolApprovalDetailsMcp : PermissionsLocationsAddToolApprovalDetails { - /// Unique identifier for tracking streamed output. - [JsonPropertyName("processId")] - public string ProcessId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "mcp"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } } -/// Shell command to run, with optional working directory and timeout in milliseconds. +/// Location-persisted tool approval details for MCP sampling requests from a server. +/// The mcp-sampling variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellExecRequest +public partial class PermissionsLocationsAddToolApprovalDetailsMcpSampling : PermissionsLocationsAddToolApprovalDetails { - /// Shell command to execute. - [JsonPropertyName("command")] - public string Command { get; set; } = string.Empty; - - /// Working directory (defaults to session working directory). - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "mcp-sampling"; - /// Timeout in milliseconds (default: 30000). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("timeout")] - public TimeSpan? Timeout { get; set; } + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Indicates whether the signal was delivered; false if the process was unknown or already exited. +/// Location-persisted tool approval details for writes to long-term memory. +/// The memory variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellKillResult +public partial class PermissionsLocationsAddToolApprovalDetailsMemory : PermissionsLocationsAddToolApprovalDetails { - /// Whether the signal was sent successfully. - [JsonPropertyName("killed")] - public bool Killed { get; set; } + /// + [JsonIgnore] + public override string Kind => "memory"; } -/// Identifier of a process previously returned by "shell.exec" and the signal to send. +/// Location-persisted tool approval details for a custom tool, keyed by tool name. +/// The custom-tool variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellKillRequest +public partial class PermissionsLocationsAddToolApprovalDetailsCustomTool : PermissionsLocationsAddToolApprovalDetails { - /// Process identifier returned by shell.exec. - [JsonPropertyName("processId")] - public string ProcessId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "custom-tool"; - /// Signal to send (default: SIGTERM). - [JsonPropertyName("signal")] - public ShellKillSignal? Signal { get; set; } + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } } -/// Result of a user-requested shell command. +/// Location-persisted tool approval details for extension-management operations, optionally narrowed by operation. +/// The extension-management variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserRequestedShellCommandResult +public partial class PermissionsLocationsAddToolApprovalDetailsExtensionManagement : PermissionsLocationsAddToolApprovalDetails { - /// Error output when the execution failed. - [JsonPropertyName("error")] - public string? Error { get; set; } - - /// Process exit code, when available. - [JsonPropertyName("exitCode")] - public long? ExitCode { get; set; } + /// + [JsonIgnore] + public override string Kind => "extension-management"; - /// Captured command output. - [JsonPropertyName("output")] - public string Output { get; set; } = string.Empty; + /// Optional operation identifier; when omitted, the approval covers all extension management operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operation")] + public string? Operation { get; set; } +} - /// Whether the command completed successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } +/// Location-persisted workflow approval, optionally narrowed by approval key. +/// The workflow variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionsLocationsAddToolApprovalDetailsWorkflow : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "workflow"; - /// Tool call id emitted for the shell execution. - [JsonPropertyName("toolCallId")] - public string ToolCallId { get; set; } = string.Empty; + /// Optional workflow operation name or canonical approval key; when omitted, the approval covers all workflow operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("approvalKey")] + public string? ApprovalKey { get; set; } } -/// User-requested shell command and cancellation handle. +/// Location-persisted tool approval details for an extension's permission-gated capability access, keyed by extension name. +/// The extension-permission-access variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellExecuteUserRequestedRequest +public partial class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess : PermissionsLocationsAddToolApprovalDetails { - /// Shell command to execute. - [JsonPropertyName("command")] - public string Command { get; set; } = string.Empty; - - /// Caller-provided cancellation handle for this execution. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "extension-permission-access"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } } -/// Cancellation result for a user-requested shell command. +/// Location-persisted tool approval details for an extension's access to sensitive environment variables, keyed by extension name and the exact set of variable names. +/// The extension-env-access variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CancelUserRequestedShellCommandResult +public partial class PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess : PermissionsLocationsAddToolApprovalDetails { - /// Whether an in-flight execution was found and signalled to cancel. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// + [JsonIgnore] + public override string Kind => "extension-env-access"; + + /// Names of the sensitive environment variables this approval covers. Values are never persisted. + [JsonPropertyName("environmentVariables")] + public required IList EnvironmentVariables { get; set; } + + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } } -/// User-requested shell execution cancellation handle. +/// Location-scoped tool approval to persist. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellCancelUserRequestedRequest +internal sealed class PermissionLocationAddToolApprovalParams { - /// Request ID previously passed to executeUserRequested. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Tool approval to persist and apply. + [JsonPropertyName("approval")] + public PermissionsLocationsAddToolApprovalDetails Approval { get => field ??= new(); set; } + + /// Location key (git root or cwd) to persist the approval to. + [JsonPropertyName("locationKey")] + public string LocationKey { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Post-compaction context window usage breakdown. +/// Folder trust check result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryCompactContextWindow +public sealed class FolderTrustCheckResult { - /// Token count from non-system messages (user, assistant, tool). - [JsonPropertyName("conversationTokens")] - public long? ConversationTokens { get; set; } - - /// Current total tokens in the context window (system + conversation + tool definitions). - [JsonPropertyName("currentTokens")] - public long CurrentTokens { get; set; } - - /// Current number of messages in the conversation. - [JsonPropertyName("messagesLength")] - public long MessagesLength { get; set; } - - /// Token count from system message(s). - [JsonPropertyName("systemTokens")] - public long? SystemTokens { get; set; } - - /// Maximum token count for the model's context window. - [JsonPropertyName("tokenLimit")] - public long TokenLimit { get; set; } - - /// Token count from tool definitions. - [JsonPropertyName("toolDefinitionsTokens")] - public long? ToolDefinitionsTokens { get; set; } + /// Whether the folder is trusted. + [JsonPropertyName("trusted")] + public bool Trusted { get; set; } } -/// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. +/// Folder path to check for trust. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryCompactResult +internal sealed class FolderTrustCheckParams { - /// Post-compaction context window usage breakdown. - [JsonPropertyName("contextWindow")] - public HistoryCompactContextWindow? ContextWindow { get; set; } - - /// Number of messages removed during compaction. - [JsonPropertyName("messagesRemoved")] - public long MessagesRemoved { get; set; } - - /// Whether compaction completed successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } - - /// Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). - [JsonPropertyName("summaryContent")] - public string? SummaryContent { get; set; } + /// Folder path to check. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Number of tokens freed by compaction. - [JsonPropertyName("tokensRemoved")] - public long TokensRemoved { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// RPC data type for SessionHistoryCompact operations. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionHistoryCompactRequest +public sealed class PermissionsFolderTrustAddTrustedResult { - /// Optional user-provided instructions to focus the compaction summary. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MaxLength(4000)] - [JsonPropertyName("customInstructions")] - public string? CustomInstructions { get; set; } - - /// Context window token limit this compaction is targeting, recorded as the `tokenLimit` on the persisted `session.compaction_start` / `session.compaction_complete` events. Set it when the compaction targets a window other than the compacting model's own, e.g. switching to a model with a smaller context window: the compaction still runs on the current model, so the limit that motivated it would otherwise be lost. When absent, the events record the compacting model's own resolved limit. Attribution metadata only - it does not change how much the compaction removes. - [JsonPropertyName("tokenLimit")] - public long? TokenLimit { get; set; } - - /// 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). - [JsonPropertyName("trigger")] - public SessionHistoryCompactRequestTrigger? Trigger { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// RPC data type for SessionHistoryCompactRequestWithSession operations. +/// Folder path to add to trusted folders. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryCompactRequestWithSession +internal sealed class FolderTrustAddParams { - /// Optional user-provided instructions to focus the compaction summary. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MaxLength(4000)] - [JsonPropertyName("customInstructions")] - public string? CustomInstructions { get; set; } + /// Folder path to mark as trusted. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Context window token limit this compaction is targeting, recorded as the `tokenLimit` on the persisted `session.compaction_start` / `session.compaction_complete` events. Set it when the compaction targets a window other than the compacting model's own, e.g. switching to a model with a smaller context window: the compaction still runs on the current model, so the limit that motivated it would otherwise be lost. When absent, the events record the compacting model's own resolved limit. Attribution metadata only - it does not change how much the compaction removes. - [JsonPropertyName("tokenLimit")] - public long? TokenLimit { get; set; } - - /// 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). - [JsonPropertyName("trigger")] - public SessionHistoryCompactRequestTrigger? Trigger { get; set; } } -/// Number of events that were removed by the truncation. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryTruncateResult +public sealed class PermissionsUrlsSetUnrestrictedModeResult { - /// Failure detail when checkpointCleanupFailed is true. - [JsonPropertyName("checkpointCleanupError")] - public string? CheckpointCleanupError { get; set; } - - /// True when conversation truncation succeeded but post-truncation workspace checkpoint cleanup failed. History is already truncated; callers may still prune snapshots but should report a checkpoint-cleanup rather than a truncation failure. - [JsonPropertyName("checkpointCleanupFailed")] - public bool? CheckpointCleanupFailed { get; set; } - - /// Number of events that were removed. - [JsonPropertyName("eventsRemoved")] - public long EventsRemoved { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Identifier of the event to truncate to; this event and all later events are removed. +/// Whether the URL-permission policy should run in unrestricted mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryTruncateRequest +internal sealed class PermissionUrlsSetUnrestrictedModeParams { - /// Event ID to truncate to. This event and all events after it are removed from the session. - [JsonPropertyName("eventId")] - public string EventId { get; set; } = string.Empty; + /// Whether to allow access to all URLs without prompting. Toggles the runtime's URL-permission policy in place. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A root user turn that the session can rewind to. +/// The repository the remote session targets. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindPoint +public sealed class MetadataSnapshotRemoteMetadataRepository { - /// Whether at least one file in this turn or a later turn can be restored. - [JsonPropertyName("canRestoreFiles")] - public bool CanRestoreFiles { get; set; } - - /// ID of the user.message event that begins the discarded suffix. - [JsonPropertyName("eventId")] - public string EventId { get; set; } = string.Empty; - - /// Number of unique files in this turn and all later turns that have captured changes. - [JsonPropertyName("fileCount")] - public long FileCount { get; set; } + /// The branch the remote session is operating on. + [JsonPropertyName("branch")] + public string Branch { get; set; } = string.Empty; - /// Whether this turn was an automatically injected autopilot continuation. - [JsonPropertyName("isAutopilotContinuation")] - public bool IsAutopilotContinuation { get; set; } + /// The GitHub repository name (without owner). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Lines added by this turn's captured file changes. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } + /// The GitHub owner (user or organization) of the target repository. + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; +} - /// Lines removed by this turn's captured file changes. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } +/// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MetadataSnapshotRemoteMetadata +{ + /// The pull request number the remote session is associated with, if any. + [JsonPropertyName("pullRequestNumber")] + public long? PullRequestNumber { get; set; } - /// ISO timestamp of the user turn. - [JsonPropertyName("timestamp")] - public string Timestamp { get; set; } = string.Empty; + /// The repository the remote session targets. + [JsonPropertyName("repository")] + public MetadataSnapshotRemoteMetadataRepository Repository { get => field ??= new(); set; } - /// Whether this turn itself captured any file changes. - [JsonPropertyName("turnChangedFiles")] - public bool TurnChangedFiles { get; set; } + /// The original resource identifier (task ID or PR node ID), preserved across event-replay reconstructions. Falls back to `sessionId` when absent. + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } - /// User-visible message text for the turn. - [JsonPropertyName("userMessage")] - public string UserMessage { get; set; } = string.Empty; + /// Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. + [JsonPropertyName("taskType")] + public MetadataSnapshotRemoteMetadataTaskType? TaskType { get; set; } } -/// Rewind points and file-change-tracking availability for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryListRewindPointsResult +/// Public-facing projection of workspace metadata for SDK / TUI consumers. +public sealed class SessionMetadataSnapshotWorkspace { - /// Whether this session captured file changes from its first turn. - [JsonPropertyName("fileChangeTrackingEnabled")] - public bool FileChangeTrackingEnabled { get; set; } + /// Branch checked out at session start, if any. + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// Root user turns in chronological order. Empty when `unavailableReason` is set. - [JsonPropertyName("points")] - public IList Points { get => field ??= []; set; } + /// ISO 8601 timestamp when the workspace was created. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } - /// Why the listed points could not be produced, when applicable; the points list is empty whenever it is set. `unsupported-remote-session` is permanent for the session and comes with `fileChangeTrackingEnabled: false`. `session-busy` is transient and only ever reported by a session that *is* tracking (`fileChangeTrackingEnabled: true`), because the file-change captures cannot be read while work that may still mutate them is in flight; the same request succeeds once the session settles, so a client that wants points should retry rather than treat it as a failure. It is never `file-change-tracking-disabled`: an untracked local session still lists conversation-only points and reports that through `fileChangeTrackingEnabled: false`. - [JsonPropertyName("unavailableReason")] - public HistoryRewindUnavailableReason? UnavailableReason { get; set; } -} + /// Current working directory at session start. + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryListRewindPointsRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Resolved git root for cwd, if any. + [JsonPropertyName("git_root")] + public string? GitRoot { get; set; } -/// A file that a conversation-and-files rewind would restore. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindFilePreview -{ - /// Aggregate change made across the discarded turns. - [JsonPropertyName("changeType")] - public HistoryRewindChangeType ChangeType { get; set; } + /// Repository host type, if known. + [JsonPropertyName("host_type")] + public WorkspaceSummaryHostType? HostType { get; set; } - /// Lines added across the discarded turns. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } + /// Workspace identifier (1:1 with sessionId). + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Lines removed across the discarded turns. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } + /// Display name for the session, if set. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Absolute path of the captured file. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Repository identifier in 'owner/repo' or 'org/project/repo' format, if any. + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + /// ISO 8601 timestamp when the workspace was last updated. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Whether the display name was explicitly set by the user. + [JsonPropertyName("user_named")] + public bool? UserNamed { get; set; } } -/// Files and aggregate changes for a prospective rewind. +/// Point-in-time snapshot of slow-changing session identifier and state fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryPreviewRewindResult +public sealed class SessionMetadataSnapshot { - /// Whether file restore is available for this session. This is authoritative: switch on it and read `reason` only when it is false. - [JsonPropertyName("available")] - public bool Available { get; set; } + /// True when the session was detected to be in use by another process at construction time. Local consumers may surface a confirmation prompt before fully attaching. Always false for new sessions. + [JsonPropertyName("alreadyInUse")] + public bool AlreadyInUse { get; set; } - /// Number of unique files in the preview. - [JsonPropertyName("fileCount")] - public long FileCount { get; set; } + /// Runtime client name associated with the session (telemetry identifier). + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// Files ordered by path. - [JsonPropertyName("files")] - public IList Files { get => field ??= []; set; } + /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot'). + [JsonPropertyName("currentMode")] + public MetadataSnapshotCurrentMode CurrentMode { get; set; } - /// Why file restore is unavailable, when applicable. Populated only when `available` is false and never set when `available` is true. - [JsonPropertyName("reason")] - public HistoryRewindUnavailableReason? Reason { get; set; } -} + /// Live indexed-search state for this session activation. Omitted by runtimes that do not expose indexed-search status; absence does not indicate enablement. + [JsonPropertyName("indexedSearch")] + public IndexedSearchState? IndexedSearch { get; set; } -/// Event boundary to preview for conversation-and-files rewind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryPreviewRewindRequest -{ - /// ID of the user.message event that begins the discarded suffix. - [JsonPropertyName("eventId")] - public string EventId { get; set; } = string.Empty; + /// User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. + [JsonPropertyName("initialName")] + public string? InitialName { get; set; } - /// Target session identifier. + /// Whether this is a remote session (i.e., one whose runtime executes elsewhere and is steered through this process). + [JsonPropertyName("isRemote")] + public bool IsRemote { get; set; } + + /// ISO 8601 timestamp of when the session's persisted state was last modified on disk. For new sessions, equals startTime. For resumed sessions, reflects the previous modification time at construction. + [JsonPropertyName("modifiedTime")] + public DateTimeOffset ModifiedTime { get; set; } + + /// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. + [JsonPropertyName("remoteMetadata")] + public MetadataSnapshotRemoteMetadata? RemoteMetadata { get; set; } + + /// Currently selected model identifier, if any. + [JsonPropertyName("selectedModel")] + public string? SelectedModel { get; set; } + + /// The unique identifier of the session. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; -} - -/// A captured file that rewind intentionally left unchanged. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistorySkippedFileRestore -{ - /// Absolute path of the skipped file. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - /// Reason the file was not restored. - [JsonPropertyName("reason")] - public HistoryFileRestoreSkipReason Reason { get; set; } -} + /// Current session limits, or null when no limits are active. + [JsonPropertyName("sessionLimits")] + public SessionLimitsConfig? SessionLimits { get; set; } -/// Structured outcome of a rewind request. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindResult -{ - /// Failure detail. Set only for the failure and partial-failure outcomes (`files-rolled-back`, `rollback-incomplete`, `truncation-failed`, `checkpoint-cleanup-failed`, `snapshot-prune-failed`); omitted for `success` and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`). - [JsonPropertyName("error")] - public string? Error { get; set; } + /// ISO 8601 timestamp of when the session started. + [JsonPropertyName("startTime")] + public DateTimeOffset StartTime { get; set; } - /// Number of persisted events removed by conversation truncation. Present only when truncation succeeded (outcomes `success`, `checkpoint-cleanup-failed`, and `snapshot-prune-failed`); omitted for every unavailable outcome (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`) and for `truncation-failed`, `files-rolled-back`, and `rollback-incomplete`. - [JsonPropertyName("eventsRemoved")] - public long? EventsRemoved { get; set; } + /// Short human-readable summary of the session, if known. Omitted when no summary has been generated. + [JsonPropertyName("summary")] + public string? Summary { get; set; } - /// Overall rewind outcome. This discriminates the result: it governs which of the remaining fields are populated, so consumers must switch on it before reading `eventsRemoved`, `restoredFiles`, `skippedFiles`, or `error`. See each field for the outcomes that populate it. - [JsonPropertyName("outcome")] - public HistoryRewindOutcome Outcome { get; set; } + /// Absolute path to the session's current working directory. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; - /// Absolute paths restored to their captured preimages. Always empty for conversation-only rewinds and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`); only conversation-and-files outcomes that reached the file-restore stage populate it. - [JsonPropertyName("restoredFiles")] - public IList RestoredFiles { get => field ??= []; set; } + /// Public-facing workspace metadata for this session, or null if the session has no associated workspace. Excludes runtime-internal fields (GitHub IDs, summary count, internal flags). + [JsonPropertyName("workspace")] + public SessionMetadataSnapshotWorkspace? Workspace { get; set; } - /// Captured files intentionally left unchanged. Always empty for conversation-only rewinds and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`); only conversation-and-files outcomes that reached the file-restore stage populate it. - [JsonPropertyName("skippedFiles")] - public IList SkippedFiles { get => field ??= []; set; } + /// Absolute path to the session's workspace directory on disk, or null if the session has no associated workspace. + [JsonPropertyName("workspacePath")] + public string? WorkspacePath { get; set; } } -/// Boundary and mode for rewinding session history. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryRewindRequest +internal sealed class SessionMetadataSnapshotRequest { - /// ID of the user.message event that begins the discarded suffix. - [JsonPropertyName("eventId")] - public string EventId { get; set; } = string.Empty; - - /// Whether to rewind only conversation history or also restore captured files. - [JsonPropertyName("mode")] - public HistoryRewindMode Mode { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether an in-progress background compaction was cancelled. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryCancelBackgroundCompactionResult +internal sealed class SessionMetadataGetClientMetadataRequest { - /// Whether an in-progress background compaction was cancelled. False when no compaction was running, when the session is remote, or when the underlying processor was unavailable. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Atomic patch for client-owned session metadata. Operations apply in clear, remove, then set order. The resulting bag must satisfy the ClientMetadata entry and serialized-size limits. Local storage coordinates concurrent runtime processes; custom SessionFs providers must serialize writers that access the same session from multiple processes. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryCancelBackgroundCompactionRequest +internal sealed class MetadataUpdateClientMetadataRequest { + /// Remove every existing client metadata entry before applying remove and set. Defaults to false. + [JsonPropertyName("clear")] + public bool? Clear { get; set; } + + /// Case-sensitive keys to remove. Missing keys are ignored. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. + [JsonPropertyName("remove")] + public IList? Remove { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// String entries to add or replace. Set wins when a key also appears in remove. Each key must be non-empty, at most 256 UTF-8 bytes, and outside the reserved `copilot/` and `github/` namespaces. Each value may contain at most 16 KiB of UTF-8 data. + [JsonPropertyName("set")] + public IDictionary? Set { get; set; } } -/// Indicates whether an in-progress manual compaction was aborted. +/// Indicates whether the local session is currently processing a turn or background continuation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryAbortManualCompactionResult +public sealed class MetadataIsProcessingResult { - /// Whether an in-progress manual compaction was aborted. False when no manual compaction was running, when its abort controller was already aborted, or when the session is remote. - [JsonPropertyName("aborted")] - public bool Aborted { get; set; } + /// Whether the session is currently processing user/agent messages. False for non-local sessions (which don't run a local agentic loop). Reflects an in-flight turn or background continuation. + [JsonPropertyName("processing")] + public bool Processing { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryAbortManualCompactionRequest +internal sealed class SessionMetadataIsProcessingRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Markdown summary of the conversation context (empty when not available). +/// Current activity flags for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistorySummarizeForHandoffResult +public sealed class SessionActivity { - /// Markdown summary of the conversation context produced by an LLM. Empty string when there are no messages or when the session does not support local summarization. - [JsonPropertyName("summary")] - public string Summary { get; set; } = string.Empty; + /// Whether an in-flight operation can currently be aborted. + [JsonPropertyName("abortable")] + public bool Abortable { get; set; } + + /// Whether the session currently has active work, including running turns or tasks. + [JsonPropertyName("hasActiveWork")] + public bool HasActiveWork { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistorySummarizeForHandoffRequest +internal sealed class SessionMetadataActivityRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryClearContextResult +/// Token-usage breakdown for the session's current context window. +public sealed class MetadataContextInfoResultContextInfo { - /// Number of non-system, non-developer messages that were removed from the conversation. Zero only when the window already held no conversation. - [JsonPropertyName("messagesCleared")] - public long MessagesCleared { get; set; } -} + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). + [JsonPropertyName("bufferTokens")] + public long BufferTokens { get; set; } -/// Parameters for clearing the conversation and seeding the window that replaces it. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryClearContextRequest -{ - /// First user message of the fresh context window. Required: a cleared window holding only system and developer messages is not a conversation a model can answer, so every clear seeds the window it creates. Delivered by the enclosing turn driver once the agentic loop exits, which is why the call must be made from inside a tool handler. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Token count at which background compaction starts (configurable percentage of promptTokenLimit). + [JsonPropertyName("compactionThreshold")] + public long CompactionThreshold { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Tokens consumed by user/assistant/tool messages. + [JsonPropertyName("conversationTokens")] + public long ConversationTokens { get; set; } -/// User-facing pending queue entry, with kind and display text for a queued message, slash command, or model change. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueuePendingItems -{ - /// Agent mode stored on this queued entry, as stamped when it was enqueued. Items without an explicit mode report interactive. This is not necessarily the mode that will constrain the turn: a plan or autopilot session applies its own write gate, continuation loop and permission posture to every drained item regardless of the mode stored here. - [JsonPropertyName("agentMode")] - public SendAgentMode AgentMode { get; set; } + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. + [JsonPropertyName("limit")] + public long Limit { get; set; } - /// Human-readable text to display for this queue entry in the UI. - [JsonPropertyName("displayText")] - public string DisplayText { get; set; } = string.Empty; + /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools). + [JsonPropertyName("mcpToolsTokens")] + public long McpToolsTokens { get; set; } - /// Stable opaque id for the canonical queued item. Batch rows share one id. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// The model used for token counting. + [JsonPropertyName("modelName")] + public string ModelName { get; set; } = string.Empty; - /// Whether this item is a queued user message or a queued slash command / model change. - [JsonPropertyName("kind")] - public QueuePendingItemsKind Kind { get; set; } + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. + [JsonPropertyName("promptTokenLimit")] + public long PromptTokenLimit { get; set; } - /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. - [JsonPropertyName("messageId")] - public string? MessageId { get; set; } + /// Tokens consumed by the system prompt. + [JsonPropertyName("systemTokens")] + public long SystemTokens { get; set; } - /// Optional source tag associated with this pending queue entry. This is an open string, not authenticated authorship. In particular, `user` does not prove that a person typed the message. If the source is absent or unrecognized, consumers must not infer human or agent authorship and should handle the entry neutrally. Consumers should tolerate future source values. - [JsonPropertyName("source")] - public string? Source { get; set; } + /// Tokens consumed by tool definitions sent to the model (excludes deferred tools). + [JsonPropertyName("toolDefinitionsTokens")] + public long ToolDefinitionsTokens { get; set; } + + /// Sum of system, conversation and tool-definition tokens. + [JsonPropertyName("totalTokens")] + public long TotalTokens { get; set; } } -/// Snapshot of the session's pending queued items and immediate-steering messages. +/// Token breakdown for the session's current context window, or null if uninitialized. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueuePendingItemsResult +public sealed class MetadataContextInfoResult { - /// 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; } - - /// Display text for messages currently in the immediate steering queue (interjections sent during a running turn). - [JsonPropertyName("steeringMessages")] - public IList SteeringMessages { get => field ??= []; set; } - - /// ID of the running turn's user message while the model has not answered it, so `withdrawMessage` can still take it back once nothing sent after it is pending. A message leaves `items` when its turn starts, before its `user.message` is recorded; this tells that message apart from one that was removed. Absent when no turn prompt can be taken back. - [JsonPropertyName("withdrawableTurnMessageId")] - public string? WithdrawableTurnMessageId { get; set; } + /// Token breakdown for the current context window, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + [JsonPropertyName("contextInfo")] + public MetadataContextInfoResultContextInfo? ContextInfo { get; set; } } -/// Identifies the target session. +/// Model identifier and token limits used to compute the context-info breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueuePendingItemsRequest +internal sealed class MetadataContextInfoRequest { + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. + [JsonPropertyName("outputTokenLimit")] + public long OutputTokenLimit { get; set; } + + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. + [JsonPropertyName("promptTokenLimit")] + public long PromptTokenLimit { get; set; } + + /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. + [JsonPropertyName("selectedModel")] + public string? SelectedModel { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Internal snapshot of native queue state for local session orchestration. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueSnapshotResult +/// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. +public sealed class MetadataContextAttributionResultContextAttributionCategories { - /// Queue item identifier of a model switch that has been dequeued but not yet applied. - [JsonPropertyName("inFlightModelChangeId")] - public string? InFlightModelChangeId { get; set; } + /// Overlapping output reservation plus post-blocking-threshold buffer. + [JsonPropertyName("buffer")] + public long Buffer { get; set; } - /// Insertion orders for queued items, aligned with `items`. - [JsonPropertyName("itemOrders")] - public IList? ItemOrders { get; set; } + /// Custom-instructions tokens (0 when none are configured). + [JsonPropertyName("customInstructions")] + public long CustomInstructions { get; set; } - /// User-facing pending items in FIFO order. - [JsonPropertyName("items")] - public IList Items { get => field ??= []; set; } + /// Remaining unused window capacity (clamped at 0). + [JsonPropertyName("freeSpace")] + public long FreeSpace { get; set; } - /// Insertion orders for immediate steering messages, aligned with `steeringMessages`. - [JsonPropertyName("steeringMessageOrders")] - public IList? SteeringMessageOrders { get; set; } + /// MCP tool-definition tokens. + [JsonPropertyName("mcpTools")] + public long McpTools { get; set; } - /// Immediate steering messages waiting for an active turn. - [JsonPropertyName("steeringMessages")] - public IList SteeringMessages { get => field ??= []; set; } -} + /// Conversation (user/assistant/tool) message tokens. + [JsonPropertyName("messages")] + public long Messages { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueSnapshotRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// System prompt tokens, excluding custom instructions. + [JsonPropertyName("systemPrompt")] + public long SystemPrompt { get; set; } -/// Result of moving a queued item. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueMoveItemResult -{ - /// True when the item changed position; false when it was already at the requested position. - [JsonPropertyName("changed")] - public bool Changed { get; set; } + /// Non-MCP tool-definition tokens. + [JsonPropertyName("systemTools")] + public long SystemTools { get; set; } } -/// Parameters for moving a queued item by stable id. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueMoveItemRequest +/// Successful compaction history for the session. +public sealed class MetadataContextAttributionResultContextAttributionCompactions { - /// Stable opaque queued-item id. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; - - /// Zero-based target position in the public visible queue. Values outside the queue clamp to an end. - [JsonPropertyName("toPosition")] - public long ToPosition { get; set; } + /// Number of successful compactions in this session. + [JsonPropertyName("count")] + public long Count { get; set; } } -/// Result of inserting a queued message. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueInsertAtResult +/// RPC data type for MetadataContextAttributionResultContextAttributionEntry operations. +public sealed class MetadataContextAttributionResultContextAttributionEntry { - /// Fresh stable opaque id assigned to the inserted item. + /// Supplementary per-entry metadata (e.g. `messageCount`, `role`, `evictable`, `pluginSource`). Values are stringified; parse as needed and ignore unrecognized keys. + [JsonPropertyName("attributes")] + public IDictionary? Attributes { get; set; } + + /// Identifier for this entry, formed by joining its `kind` and source name (e.g. `tool:bash`, `skill:tmux`, `toolDefinition:bash`); unique within the snapshot. Use it to match the same entry across snapshots, to correlate with other APIs (skill/agent/MCP registries), and as the `parentId` target for nesting. Distinct from the human-facing `label`. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; -} - -/// Serializable message fields accepted by queue.insertAt. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueInsertMessage -{ - /// Optional explicit agent mode. When omitted, the session's current mode is assigned. - [JsonPropertyName("agentMode")] - public SendAgentMode? AgentMode { get; set; } - /// Optional attachments for the message. - [JsonPropertyName("attachments")] - public IList? Attachments { get; set; } + /// Source category for this entry. Not a closed set — tolerate unknown values. Known values today: `skill`, `subagent`, `mcpServer`, `tool`, `system`, `toolDefinition`, `plugin`. + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; - /// Whether the message is billable. - [JsonPropertyName("billable")] - public bool? Billable { get; set; } + /// Human-readable display label, e.g. `bash` or `skill: tmux`. Presentation-only; may be localized/reformatted without notice — do not key off it. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; - /// Accepted for internal SendOptions compatibility but ignored; delivery is derived from current session activity. - [JsonPropertyName("delivery")] - public string? Delivery { get; set; } + /// Optional `id` of the parent entry: e.g. a `plugin` entry parenting its `skill`/`mcpServer` entries, or the `system` entry parenting `toolDefinition` entries. Omitted for top-level entries. + [JsonPropertyName("parentId")] + public string? ParentId { get; set; } - /// Optional user-facing display text. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// Token count currently in context attributable to this entry. + [JsonPropertyName("tokens")] + public long Tokens { get; set; } +} - /// Accepted for SendOptions compatibility but ignored; inserted items always use queued delivery semantics. - [JsonPropertyName("mode")] - public SendMode? Mode { get; set; } +/// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. +public sealed class MetadataContextAttributionResultContextAttribution +{ + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + [JsonPropertyName("bufferTokens")] + public long BufferTokens { get; set; } - /// Accepted for SendOptions compatibility but ignored; the requested public position controls placement. - [JsonPropertyName("prepend")] - public bool? Prepend { get; set; } + /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. + [JsonPropertyName("categories")] + public MetadataContextAttributionResultContextAttributionCategories Categories { get => field ??= new(); set; } - /// The user message text. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Successful compaction history for the session. + [JsonPropertyName("compactions")] + public MetadataContextAttributionResultContextAttributionCompactions Compactions { get => field ??= new(); set; } - /// Per-turn request headers. - [JsonPropertyName("requestHeaders")] - public IDictionary? RequestHeaders { get; set; } + /// Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. + [JsonPropertyName("compactionThreshold")] + public long CompactionThreshold { get; set; } - /// Required tool name for the turn, when any. - [JsonPropertyName("requiredTool")] - public string? RequiredTool { get; set; } + /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } - /// Optional provenance source. `system` is rejected: it would hide the inserted row from `pendingItems` and make it unaddressable while still executing, so inserted items must stay visible. - [JsonPropertyName("source")] - public string? Source { get; set; } + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. + [JsonPropertyName("limit")] + public long Limit { get; set; } - /// Accepted for SendOptions compatibility but ignored; insertion scheduling is controlled by the queue drain state. - [JsonPropertyName("wait")] - public bool? Wait { get; set; } -} + /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; -/// Parameters for inserting a queued message at a public visible position. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueInsertAtRequest -{ - /// Queued message contents and delivery metadata. - [JsonPropertyName("message")] - public QueueInsertMessage Message { get => field ??= new(); set; } + /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). + [JsonPropertyName("modelSource")] + public string ModelSource { get; set; } = string.Empty; - /// Zero-based position in the public visible queue. Values outside the queue clamp to an end. - [JsonPropertyName("position")] - public long Position { get; set; } + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. + [JsonPropertyName("promptTokenLimit")] + public long PromptTokenLimit { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. + [JsonPropertyName("totalTokens")] + public long TotalTokens { get; set; } } -/// Result of removing a queued item. +/// Per-source attribution breakdown for the session's current context window, or null if uninitialized. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueRemoveAtResult +public sealed class MetadataContextAttributionResult { - /// True when the addressed item was removed. - [JsonPropertyName("removed")] - public bool Removed { get; set; } + /// Per-source context-window attribution, or null if the session has not yet been initialized (no system prompt or tool metadata cached). + [JsonPropertyName("contextAttribution")] + public MetadataContextAttributionResultContextAttribution? ContextAttribution { get; set; } } -/// Parameters for removing a queued item by stable id. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueRemoveAtRequest +internal sealed class SessionMetadataGetContextAttributionRequest { - /// Stable opaque ID of the queued item to remove. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of editing a queued message. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueUpdateTextResult -{ - /// True when the stored text changed. - [JsonPropertyName("updated")] - public bool Updated { get; set; } -} - -/// Parameters for editing a single queued message. +/// A single large message currently in context. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueUpdateTextRequest +public sealed class ContextHeaviestMessage { - /// Optional replacement prompt displayed to the user. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } - - /// Stable opaque ID of the queued item to edit. + /// Stable identifier for this message within the snapshot. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Replacement prompt sent to the model. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Human-readable source label, e.g. `tool: bash` or `skill: tmux`. Presentation-only. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Role of the chat message (`user`, `assistant`, or `tool`). + [JsonPropertyName("role")] + public string Role { get; set; } = string.Empty; + + /// Token count currently in context for this individual message. + [JsonPropertyName("tokens")] + public long Tokens { get; set; } } -/// Result of withdrawing a user message. +/// The heaviest individual messages in the session's context window, most-expensive first. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueWithdrawMessageResult +public sealed class MetadataContextHeaviestMessagesResult { - /// True when the running turn was interrupted to withdraw the message. With removed false, the turn was interrupted but its events could not be removed, for example because the model answered first, so the message stays in the interrupted turn. - [JsonPropertyName("interrupted")] - public bool Interrupted { get; set; } + /// Heaviest messages, most-expensive first. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; set; } - /// True when the message left the queue or, for a running turn, history. - [JsonPropertyName("removed")] - public bool Removed { get; set; } + /// Total token count of the current context window, so callers can compute each message's share without a second call. + [JsonPropertyName("totalTokens")] + public long TotalTokens { get; set; } } -/// Conditional withdrawal of a single user message, from its queue or from the running turn it started. +/// Parameters for the heaviest-messages query. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueWithdrawMessageRequest +internal sealed class MetadataContextHeaviestMessagesRequest { - /// The prompt originally sent. A message edited since submission is not withdrawn, so an obsolete draft cannot replace the edit. - [JsonPropertyName("expectedPrompt")] - public string ExpectedPrompt { get; set; } = string.Empty; - - /// Message identity returned by send, not the queue item id. Batch messages are not eligible. - [JsonPropertyName("messageId")] - public string MessageId { get; set; } = string.Empty; + /// Maximum number of messages to return, most-expensive first. Omit for the server default. + [JsonPropertyName("limit")] + public long? Limit { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Append to one pending steering message without changing its identity or delivery position. +/// Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event); move a local session's working directory via `metadata.setWorkingDirectory` instead. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueAppendSteeringRequest +public sealed class MetadataRecordContextChangeResult { - /// Mode captured at submission. Only steering messages in the same mode may be combined. - [JsonPropertyName("agentMode")] - public SendAgentMode AgentMode { get; set; } - - /// Attachments to add after the message's existing attachments. An empty list preserves the existing attachments. - [JsonPropertyName("attachments")] - public IList Attachments { get => field ??= []; set; } - - /// Display text to append to the existing preview after a blank line. - [JsonPropertyName("displayPrompt")] - public string DisplayPrompt { get; set; } = string.Empty; - - /// Expected current prompt, including any previous appends. The runtime applies plan-mode normalization before comparing and refuses a changed message. - [JsonPropertyName("expectedPrompt")] - public string ExpectedPrompt { get; set; } = string.Empty; - - /// Message identity returned by send, not the queue item id. Only unclaimed user steering messages are eligible. - [JsonPropertyName("messageId")] - public string MessageId { get; set; } = string.Empty; +} - /// Text to append after a blank line. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; +/// Updated working-directory/git context to record on the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class MetadataRecordContextChangeRequest +{ + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + [JsonPropertyName("context")] + public SessionWorkingDirectoryContext Context { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of duplicating a queued item. +/// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueDuplicateAtResult +public sealed class MetadataSetWorkingDirectoryResult { - /// Fresh stable opaque id assigned to the duplicate. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Working directory after the update. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; } -/// Parameters for duplicating a queued item. +/// Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueDuplicateAtRequest +internal sealed class MetadataSetWorkingDirectoryRequest { - /// Stable opaque ID of the queued item to duplicate. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Absolute path to set as the session's working directory. The runtime updates the session's recorded cwd so subsequent operations (shell tools, file lookups, telemetry) anchor to it. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; } -/// Parameters for acquiring or releasing the queued-lane drain pause. Acquisition is exclusive and non-idempotent: `paused: true` against an already-paused session fails with `queue_already_paused`. The pause is never released automatically — it is not tied to the caller's lifetime, so a client that exits without sending `paused: false` leaves the lane frozen. Release is unowned: `paused: false` clears the pause for any caller, including one that never acquired it. +/// Re-tokenize the session's existing messages against `modelId` and return the token totals. Useful for hosts that want an initial estimate of context usage on session resume, before the next agent turn fires `session.context_info_changed` events. Returns zeros for an empty session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueSetDrainPausedRequest +public sealed class MetadataRecomputeContextTokensResult { - /// Whether queued-lane draining should be paused. - [JsonPropertyName("paused")] - public bool Paused { get; set; } + /// Tokens contributed by user/assistant/tool messages (excludes system/developer prompts). + [JsonPropertyName("messagesTokenCount")] + public long MessagesTokenCount { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Tokens contributed by system/developer prompt snapshots. + [JsonPropertyName("systemTokenCount")] + public long SystemTokenCount { get; set; } -/// Result of trying to steer a queued message into a live turn. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueSendNowResult -{ - /// True when the item was accepted into the steering lane; false when no main turn was live. - [JsonPropertyName("steered")] - public bool Steered { get; set; } + /// Sum of tokens across chat-context and system-context messages currently held by the session. + [JsonPropertyName("totalTokens")] + public long TotalTokens { get; set; } } -/// Parameters for steering a queued message into a live turn. +/// Model identifier to use when re-tokenizing the session's existing messages. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueSendNowRequest +internal sealed class MetadataRecomputeContextTokensRequest { - /// Stable opaque ID of the queued item to steer into the live turn. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Model identifier used for tokenization. The runtime token-counts both chat-context and system-context messages against this model. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Whether the native queue has pending work. +/// Availability of built-in job tools surfaced to boundary consumers. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueHasPendingResult +public sealed class SessionSettingsBuiltInToolAvailabilitySnapshot { - /// True when queued or immediate native work is pending. - [JsonPropertyName("hasPending")] - public bool HasPending { get; set; } -} + /// Whether the create-pull-request tool is available. + [JsonPropertyName("createPullRequest")] + public bool? CreatePullRequest { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueHasPendingRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the report-progress tool is available. + [JsonPropertyName("reportProgress")] + public bool? ReportProgress { get; set; } } -/// Whether a deferred-idle drain should run. +/// Redacted job settings for a session. The job nonce is excluded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueBeginDeferredIdleDrainResult +public sealed class SessionSettingsJobSnapshot { - /// True when the host should run finishDeferredIdleDrain asynchronously. - [JsonPropertyName("shouldDrain")] - public bool ShouldDrain { get; set; } -} + /// Availability of job-specific built-in tools. + [JsonPropertyName("builtInToolAvailability")] + public SessionSettingsBuiltInToolAvailabilitySnapshot? BuiltInToolAvailability { get; set; } -/// Inputs for starting a deferred-idle drain. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueBeginDeferredIdleDrainRequest -{ - /// Whether the host still has active background work. - [JsonPropertyName("activeBackgroundWork")] - public bool ActiveBackgroundWork { get; set; } + /// GitHub Actions event type for the job. + [JsonPropertyName("eventType")] + public string? EventType { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether this is the workflow's trigger job. + [JsonPropertyName("isTriggerJob")] + public bool? IsTriggerJob { get; set; } } -/// Action selected by the native deferred-idle drain. +/// Redacted model routing settings for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueFinishDeferredIdleDrainResult +public sealed class SessionSettingsModelSnapshot { - /// Whether the deferred idle was caused by an aborted foreground turn. - [JsonPropertyName("aborted")] - public bool Aborted { get; set; } + /// Agent service callback URL for job and progress updates. + [JsonPropertyName("callbackUrl")] + public string? CallbackUrl { get; set; } - /// One of none, processQueue, or emitSessionIdle. - [JsonPropertyName("action")] - public string Action { get; set; } = string.Empty; + /// Default reasoning effort for the selected model. + [JsonPropertyName("defaultReasoningEffort")] + public string? DefaultReasoningEffort { get; set; } + + /// Agent job identifier for the session. + [JsonPropertyName("instanceId")] + public string? InstanceId { get; set; } + + /// Selected model identifier. + [JsonPropertyName("model")] + public string? Model { get; set; } } -/// Inputs for completing a deferred-idle drain. +/// Online-evaluation settings safe to expose across the SDK boundary. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueFinishDeferredIdleDrainRequest +public sealed class SessionSettingsOnlineEvaluationSnapshot { - /// Whether the host still has active background work. - [JsonPropertyName("activeBackgroundWork")] - public bool ActiveBackgroundWork { get; set; } + /// Whether online evaluation is disabled. + [JsonPropertyName("disableOnlineEvaluation")] + public bool? DisableOnlineEvaluation { get; set; } - /// Whether native queued work remains. - [JsonPropertyName("hasPending")] - public bool HasPending { get; set; } + /// Whether online-evaluation output-file generation is enabled. + [JsonPropertyName("enableOnlineEvaluationOutputFile")] + public bool? EnableOnlineEvaluationOutputFile { get; set; } +} - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// Redacted repository and GitHub host settings for a session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionSettingsRepoSnapshot +{ + /// Checked-out repository branch. + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + /// Checked-out commit SHA. + [JsonPropertyName("commit")] + public string? Commit { get; set; } + + /// GitHub server host name. + [JsonPropertyName("host")] + public string? Host { get; set; } + + /// Protocol used to access the GitHub host. + [JsonPropertyName("hostProtocol")] + public string? HostProtocol { get; set; } + + /// GitHub repository database ID. + [JsonPropertyName("id")] + public double? Id { get; set; } + + /// Repository name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// GitHub repository owner database ID. + [JsonPropertyName("ownerId")] + public double? OwnerId { get; set; } + + /// Repository owner login. + [JsonPropertyName("ownerName")] + public string? OwnerName { get; set; } + + /// Number of commits in the pull request. + [JsonPropertyName("prCommitCount")] + public double? PrCommitCount { get; set; } + + /// Whether the repository is writable. + [JsonPropertyName("readWrite")] + public bool? ReadWrite { get; set; } + + /// GitHub secret-scanning service URL. + [JsonPropertyName("secretScanningUrl")] + public string? SecretScanningUrl { get; set; } + + /// GitHub server base URL. + [JsonPropertyName("serverUrl")] + public string? ServerUrl { get; set; } } -/// Inputs for marking session.idle deferred in native state. +/// Redacted validation and memory-tool settings for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueDeferSessionIdleRequest +public sealed class SessionSettingsValidationSnapshot { - /// Whether the deferred idle was caused by an aborted foreground turn. - [JsonPropertyName("aborted")] - public bool Aborted { get; set; } + /// Whether advisory validation is enabled. + [JsonPropertyName("advisoryEnabled")] + public bool? AdvisoryEnabled { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether CodeQL validation is enabled. + [JsonPropertyName("codeqlEnabled")] + public bool? CodeqlEnabled { get; set; } + + /// Whether code-review validation is enabled. + [JsonPropertyName("codeReviewEnabled")] + public bool? CodeReviewEnabled { get; set; } + + /// Model used for code-review validation. + [JsonPropertyName("codeReviewModel")] + public string? CodeReviewModel { get; set; } + + /// Dependabot validation timeout budget in seconds. + [JsonPropertyName("dependabotTimeout")] + public double? DependabotTimeout { get; set; } + + /// Whether the memory-store tool is enabled. + [JsonPropertyName("memoryStoreEnabled")] + public bool? MemoryStoreEnabled { get; set; } + + /// Whether the memory-vote tool is enabled. + [JsonPropertyName("memoryVoteEnabled")] + public bool? MemoryVoteEnabled { get; set; } + + /// Whether secret-scanning validation is enabled. + [JsonPropertyName("secretScanningEnabled")] + public bool? SecretScanningEnabled { get; set; } + + /// General validation timeout budget in seconds. + [JsonPropertyName("timeout")] + public double? Timeout { get; set; } } -/// Indicates whether a user-facing pending item was removed. +/// Redacted, serializable view of session runtime settings for SDK boundary consumers. Secrets and raw feature flags are intentionally excluded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueRemoveMostRecentResult +internal sealed class SessionSettingsSnapshot { - /// True if a user-facing pending item was removed (LIFO across both queues); false when no removable items remained. - [JsonPropertyName("removed")] - public bool Removed { get; set; } + /// Name of the SDK client that created the session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// Redacted job settings. + [JsonPropertyName("job")] + public SessionSettingsJobSnapshot Job { get => field ??= new(); set; } + + /// Redacted model routing settings. + [JsonPropertyName("model")] + public SessionSettingsModelSnapshot Model { get => field ??= new(); set; } + + /// Online-evaluation settings safe for SDK consumers. + [JsonPropertyName("onlineEvaluation")] + public SessionSettingsOnlineEvaluationSnapshot OnlineEvaluation { get => field ??= new(); set; } + + /// Redacted repository and host settings. + [JsonPropertyName("repo")] + public SessionSettingsRepoSnapshot Repo { get => field ??= new(); set; } + + /// Session start time as Unix epoch milliseconds. + [JsonPropertyName("startTimeMs")] + public double? StartTimeMs { get; set; } + + /// Session timeout in milliseconds. + [JsonPropertyName("timeoutMs")] + public double? TimeoutMs { get; set; } + + /// Redacted validation and memory-tool settings. + [JsonPropertyName("validation")] + public SessionSettingsValidationSnapshot Validation { get => field ??= new(); set; } + + /// Agent runtime version selector copied from the session settings, such as `latest` or a runtime release identifier. + [JsonPropertyName("version")] + public string? Version { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueRemoveMostRecentRequest +internal sealed class SessionSettingsSnapshotRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Result of evaluating a Rust-owned settings predicate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueClearRequest +internal sealed class SessionSettingsEvaluatePredicateResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the named settings predicate evaluated to enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } } -/// Internal filter for consuming queued system notifications. +/// Named Rust-owned settings predicate to evaluate for this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueConsumeSystemNotificationsRequest +internal sealed class SessionSettingsEvaluatePredicateRequest { - /// Opaque runtime-owned filter object. - [JsonPropertyName("filter")] - public JsonElement Filter { get; set; } + /// Predicate name. The runtime owns the raw feature-flag names and composition logic. + [JsonPropertyName("name")] + public SessionSettingsPredicateName Name { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Tool name for tool-scoped predicates such as trivial-change handling. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } } -/// Result of enqueueing the resume-pending wake item. +/// Content-exclusion decision for one requested path. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueEnqueueResumePendingResult +public sealed class ContentExclusionPathCheck { - /// True when a wake item was newly queued. - [JsonPropertyName("queued")] - public bool Queued { get; set; } + /// Whether the session's complete content-exclusion policy excludes the path. + [JsonPropertyName("excluded")] + public bool Excluded { get; set; } + + /// The path supplied by the caller. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; } -/// Identifies the target session. +/// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueEnqueueResumePendingRequest +public sealed class ContentExclusionCheckPathsResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Whether the session's policy service was available for the complete batch. When false, checks is empty and callers must treat every requested path as excluded. + [JsonPropertyName("available")] + public bool Available { get; set; } + + /// Per-path decisions in request order. Empty when available is false. + [JsonPropertyName("checks")] + public IList Checks { get => field ??= []; set; } } -/// Identifies the target session. +/// Local file system absolute paths within the session working directory to check against its content-exclusion policy. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueProcessRequest +internal sealed class ContentExclusionCheckPathsRequest { + /// Local file system absolute paths within the session working directory to check. Results are returned in the same order, including duplicates. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Cursor, batch size, and optional long-poll/filter parameters for reading session events. +/// Identifier of the spawned shell process, usable with shell.kill while the process is running. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EventLogReadRequest +public sealed class ShellExecResult { - /// Optional non-empty list of subagent identifiers. When provided, only events owned by one of these agents are returned; ownership recognizes the event envelope's agentId plus legacy data.agentId and data.parentToolCallId markers. This filter takes precedence over agentScope. - [JsonPropertyName("agentIds")] - public IList? AgentIds { get; set; } - - /// Agent-scope filter: 'primary' returns only main-agent events plus events whose type starts with 'subagent.' (matching the typed-subscription default behavior); 'all' returns events from all agents (matching wildcard-subscription behavior). Default is 'all' to preserve wildcard semantics for catch-up callers. - [JsonPropertyName("agentScope")] - public EventsAgentScope? AgentScope { get; set; } - - /// Opaque cursor returned by a previous read. Omit on the first call to start from the beginning of the session's persisted history. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } - - /// Direction to page through the session's persisted event history. 'forward' (default) pages from the cursor toward newer events (or from the start of history when no cursor is given). 'backward' enables tail-first reads: with no cursor it returns the NEWEST `max` events, and the returned cursor pages toward OLDER events on subsequent backward reads. Events within a returned batch are always in chronological (oldest-to-newest) order, even for a backward read. Backward reads cover PERSISTED history only; ephemeral events are never returned by a backward read. `direction` selects the INITIAL read only: the returned cursor is self-describing, so a continuation read pages in the cursor's own direction regardless of the `direction` passed alongside it — a forward cursor always pages forward and a backward cursor always pages backward. Pass the direction that matches the cursor to avoid confusion. - [JsonPropertyName("direction")] - public EventsReadDirection? Direction { get; set; } + /// Identifier usable with shell.kill while the process is running. + [JsonPropertyName("processId")] + public string ProcessId { get; set; } = string.Empty; +} - /// When false, skip ephemeral events entirely and return only durable (persisted) events. History-backfill callers that discard ephemerals anyway should set this so the read is bounded by the durable log length instead of racing the ephemeral ring on a busy session. Defaults to true (ephemerals are interleaved with durable events in creation order). Ignored by backward reads, which always cover persisted history only. - [JsonPropertyName("includeEphemeral")] - public bool? IncludeEphemeral { get; set; } +/// Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ShellExecRequest +{ + /// Shell command to execute. + [JsonPropertyName("command")] + public string Command { get; set; } = string.Empty; - /// Maximum number of events to return in this batch (1–1000, default 200). - [JsonPropertyName("max")] - public long? Max { get; set; } + /// Working directory (defaults to session working directory). + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Either '*' to receive all event types, or a non-empty list of event types to receive. - [JsonPropertyName("types")] - public JsonElement? Types { get; set; } - - /// Milliseconds to wait for new events when the cursor is at the tail of history. 0 (default) returns immediately even if no events are available. Capped at 30000ms. Ephemeral events that arrive during the wait are delivered in this batch but are NOT replayable on a subsequent read (use a non-zero waitMs in your next call to capture future ephemerals as they happen). This applies to forward reads only: a backward read always returns immediately and ignores `waitMs`, because backward paging covers persisted history only while new events append at the tail (the opposite end from a backward page), so no blocking or ephemeral delivery can occur. + /// Timeout in milliseconds (default: 30000). [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("waitMs")] - public TimeSpan? Wait { get; set; } + [JsonPropertyName("timeout")] + public TimeSpan? Timeout { get; set; } } -/// Snapshot of the current tail cursor without returning any events. Use this when a consumer wants to subscribe to live events going forward without first paginating through the entire persisted history (which would happen if `read` were called without a cursor on a long-lived session). +/// Indicates whether the signal was delivered; false if the process was unknown or already exited. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class EventLogTailResult +public sealed class ShellKillResult { - /// Opaque cursor pointing at the current tail of the session's persisted-events history. Pass back to `read` to receive only events that arrive AFTER this snapshot. When the session has no events, this returns the same sentinel as an unset cursor (i.e. equivalent to omitting the cursor on a first read). - [JsonPropertyName("cursor")] - public string Cursor { get; set; } = string.Empty; + /// Whether the signal was sent successfully. + [JsonPropertyName("killed")] + public bool Killed { get; set; } } -/// Identifies the target session. +/// Identifier of a process previously returned by "shell.exec" and the signal to send. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionEventLogTailRequest +internal sealed class ShellKillRequest { + /// Process identifier returned by shell.exec. + [JsonPropertyName("processId")] + public string ProcessId { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Signal to send (default: SIGTERM). + [JsonPropertyName("signal")] + public ShellKillSignal? Signal { get; set; } } -/// Opaque handle representing an event-type interest registration. +/// Result of a user-requested shell command. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RegisterEventInterestResult +public sealed class UserRequestedShellCommandResult { - /// Opaque handle for this registration. Pass to releaseInterest to release. Each call to registerInterest produces a fresh handle, even when the same eventType is registered multiple times. - [JsonPropertyName("handle")] - public string Handle { get; set; } = string.Empty; + /// Error output when the execution failed. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Process exit code, when available. + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Captured command output. + [JsonPropertyName("output")] + public string Output { get; set; } = string.Empty; + + /// Whether the command completed successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } + + /// Tool call id emitted for the shell execution. + [JsonPropertyName("toolCallId")] + public string ToolCallId { get; set; } = string.Empty; } -/// Event type to register consumer interest for, used by runtime gating logic. +/// User-requested shell command and cancellation handle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class RegisterEventInterestParams +internal sealed class ShellExecuteUserRequestedRequest { - /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest is registered the runtime still attempts non-interactive reconnect from cached or refreshable tokens, and only marks the server `needs-auth` if usable credentials are unavailable — it does not open a browser or start interactive OAuth without a consumer). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. - [JsonPropertyName("eventType")] - public string EventType { get; set; } = string.Empty; + /// Shell command to execute. + [JsonPropertyName("command")] + public string Command { get; set; } = string.Empty; + + /// Caller-provided cancellation handle for this execution. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// Cancellation result for a user-requested shell command. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class EventLogReleaseInterestResult +public sealed class CancelUserRequestedShellCommandResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Whether an in-flight execution was found and signalled to cancel. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// Opaque handle previously returned by `registerInterest` to release. +/// User-requested shell execution cancellation handle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ReleaseEventInterestParams +internal sealed class ShellCancelUserRequestedRequest { - /// Handle returned by a previous `registerInterest` call. Idempotent: releasing an unknown or already-released handle is a no-op (returns success). When the last outstanding handle for an event type is released, the runtime reverts to its 'no consumer' code path for that event type. - [JsonPropertyName("handle")] - public string Handle { get; set; } = string.Empty; + /// Request ID previously passed to executeUserRequested. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Request count and cost metrics for this model. +/// Post-compaction context window usage breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricRequests +public sealed class HistoryCompactContextWindow { - /// User-initiated premium request cost (with multiplier applied). - [JsonPropertyName("cost")] - public double Cost { get; set; } + /// Token count from non-system messages (user, assistant, tool). + [JsonPropertyName("conversationTokens")] + public long? ConversationTokens { get; set; } - /// Number of API requests made with this model. - [JsonPropertyName("count")] - public long Count { get; set; } -} + /// Current total tokens in the context window (system + conversation + tool definitions). + [JsonPropertyName("currentTokens")] + public long CurrentTokens { get; set; } -/// Per-model token-detail entry containing the accumulated token count for one token type. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricTokenDetail -{ - /// Accumulated token count for this token type. - [JsonPropertyName("tokenCount")] - public long TokenCount { get; set; } + /// Current number of messages in the conversation. + [JsonPropertyName("messagesLength")] + public long MessagesLength { get; set; } + + /// Token count from system message(s). + [JsonPropertyName("systemTokens")] + public long? SystemTokens { get; set; } + + /// Maximum token count for the model's context window. + [JsonPropertyName("tokenLimit")] + public long TokenLimit { get; set; } + + /// Token count from tool definitions. + [JsonPropertyName("toolDefinitionsTokens")] + public long? ToolDefinitionsTokens { get; set; } } -/// Token usage metrics for this model. +/// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricUsage +public sealed class HistoryCompactResult { - /// Total tokens read from prompt cache. - [JsonPropertyName("cacheReadTokens")] - public long CacheReadTokens { get; set; } + /// Post-compaction context window usage breakdown. + [JsonPropertyName("contextWindow")] + public HistoryCompactContextWindow? ContextWindow { get; set; } - /// Total tokens written to prompt cache. - [JsonPropertyName("cacheWriteTokens")] - public long CacheWriteTokens { get; set; } + /// Number of messages removed during compaction. + [JsonPropertyName("messagesRemoved")] + public long MessagesRemoved { get; set; } - /// Total input tokens consumed. - [JsonPropertyName("inputTokens")] - public long InputTokens { get; set; } + /// Whether compaction completed successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } - /// Total output tokens produced. - [JsonPropertyName("outputTokens")] - public long OutputTokens { get; set; } + /// Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). + [JsonPropertyName("summaryContent")] + public string? SummaryContent { get; set; } - /// Total output tokens used for reasoning. - [JsonPropertyName("reasoningTokens")] - public long? ReasoningTokens { get; set; } + /// Number of tokens freed by compaction. + [JsonPropertyName("tokensRemoved")] + public long TokensRemoved { get; set; } } -/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. +/// RPC data type for SessionHistoryCompact operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetric +public sealed class SessionHistoryCompactRequest { - /// Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. - [JsonPropertyName("cacheExpiresAt")] - public DateTimeOffset? CacheExpiresAt { get; set; } - - /// Request count and cost metrics for this model. - [JsonPropertyName("requests")] - public UsageMetricsModelMetricRequests Requests { get => field ??= new(); set; } - - /// Token count details per type. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Optional user-provided instructions to focus the compaction summary. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MaxLength(4000)] + [JsonPropertyName("customInstructions")] + public string? CustomInstructions { get; set; } - /// Accumulated nano-AI units cost for this model. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { get; set; } + /// Context window token limit this compaction is targeting, recorded as the `tokenLimit` on the persisted `session.compaction_start` / `session.compaction_complete` events. Set it when the compaction targets a window other than the compacting model's own, e.g. switching to a model with a smaller context window: the compaction still runs on the current model, so the limit that motivated it would otherwise be lost. When absent, the events record the compacting model's own resolved limit. Attribution metadata only - it does not change how much the compaction removes. + [JsonPropertyName("tokenLimit")] + public long? TokenLimit { get; set; } - /// Token usage metrics for this model. - [JsonPropertyName("usage")] - public UsageMetricsModelMetricUsage Usage { get => field ??= new(); set; } + /// 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). + [JsonPropertyName("trigger")] + public SessionHistoryCompactRequestTrigger? Trigger { get; set; } } -/// Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. +/// RPC data type for SessionHistoryCompactRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsAgentMetric +internal sealed class SessionHistoryCompactRequestWithSession { - /// Human-readable label for this subagent invocation, copied from the originating `subagent.started` event. For task-tool subagents this is the invocation's task description rather than the agent's configured display name, so group by `agentName` for stable per-agent labels. - [JsonPropertyName("agentDisplayName")] - public string? AgentDisplayName { get; set; } - - /// Configured agent name, when this is a subagent. - [JsonPropertyName("agentName")] - public string? AgentName { get; set; } + /// Optional user-provided instructions to focus the compaction summary. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MaxLength(4000)] + [JsonPropertyName("customInstructions")] + public string? CustomInstructions { get; set; } - /// Per-model usage for this agent, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Time spent in model API calls by this agent, in milliseconds. - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// Context window token limit this compaction is targeting, recorded as the `tokenLimit` on the persisted `session.compaction_start` / `session.compaction_complete` events. Set it when the compaction targets a window other than the compacting model's own, e.g. switching to a model with a smaller context window: the compaction still runs on the current model, so the limit that motivated it would otherwise be lost. When absent, the events record the compacting model's own resolved limit. Attribution metadata only - it does not change how much the compaction removes. + [JsonPropertyName("tokenLimit")] + public long? TokenLimit { get; set; } - /// Accumulated nano-AI units cost for this agent. - [JsonPropertyName("totalNanoAiu")] - public double TotalNanoAiu { get; set; } + /// 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). + [JsonPropertyName("trigger")] + public SessionHistoryCompactRequestTrigger? Trigger { get; set; } } -/// Aggregated code change metrics. +/// Number of events that were removed by the truncation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsCodeChanges +public sealed class HistoryTruncateResult { - /// Distinct file paths modified during the session. - [JsonPropertyName("filesModified")] - public IList FilesModified { get => field ??= []; set; } - - /// Number of distinct files modified. - [JsonPropertyName("filesModifiedCount")] - public long FilesModifiedCount { get; set; } + /// Failure detail when checkpointCleanupFailed is true. + [JsonPropertyName("checkpointCleanupError")] + public string? CheckpointCleanupError { get; set; } - /// Total lines of code added. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } + /// True when conversation truncation succeeded but post-truncation workspace checkpoint cleanup failed. History is already truncated; callers may still prune snapshots but should report a checkpoint-cleanup rather than a truncation failure. + [JsonPropertyName("checkpointCleanupFailed")] + public bool? CheckpointCleanupFailed { get; set; } - /// Total lines of code removed. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } + /// Number of events that were removed. + [JsonPropertyName("eventsRemoved")] + public long EventsRemoved { get; set; } } -/// Session-wide token-detail entry containing the accumulated token count for one token type. +/// Identifier of the event to truncate to; this event and all later events are removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsTokenDetail +internal sealed class HistoryTruncateRequest { - /// Accumulated token count for this token type. - [JsonPropertyName("tokenCount")] - public long TokenCount { get; set; } + /// Event ID to truncate to. This event and all events after it are removed from the session. + [JsonPropertyName("eventId")] + public string EventId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. +/// A root user turn that the session can rewind to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageGetMetricsResult +public sealed class HistoryRewindPoint { - /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. - [JsonPropertyName("agentMetrics")] - public IDictionary? AgentMetrics { get; set; } + /// Whether at least one file in this turn or a later turn can be restored. + [JsonPropertyName("canRestoreFiles")] + public bool CanRestoreFiles { get; set; } - /// Aggregated code change metrics. - [JsonPropertyName("codeChanges")] - public UsageMetricsCodeChanges CodeChanges { get => field ??= new(); set; } + /// ID of the user.message event that begins the discarded suffix. + [JsonPropertyName("eventId")] + public string EventId { get; set; } = string.Empty; - /// Currently active model identifier. - [JsonPropertyName("currentModel")] - public string? CurrentModel { get; set; } + /// Number of unique files in this turn and all later turns that have captured changes. + [JsonPropertyName("fileCount")] + public long FileCount { get; set; } - /// Input tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallInputTokens")] - public long LastCallInputTokens { get; set; } + /// Whether this turn was an automatically injected autopilot continuation. + [JsonPropertyName("isAutopilotContinuation")] + public bool IsAutopilotContinuation { get; set; } - /// Output tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallOutputTokens")] - public long LastCallOutputTokens { get; set; } + /// Lines added by this turn's captured file changes. + [JsonPropertyName("linesAdded")] + public long LinesAdded { get; set; } - /// Per-model token and request metrics, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + /// Lines removed by this turn's captured file changes. + [JsonPropertyName("linesRemoved")] + public long LinesRemoved { get; set; } - /// ISO 8601 timestamp when the session started. - [JsonPropertyName("sessionStartTime")] - public DateTimeOffset SessionStartTime { get; set; } + /// ISO timestamp of the user turn. + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } = string.Empty; - /// Session-wide per-token-type accumulated token counts. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Whether this turn itself captured any file changes. + [JsonPropertyName("turnChangedFiles")] + public bool TurnChangedFiles { get; set; } - /// Total time spent in model API calls (milliseconds). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// User-visible message text for the turn. + [JsonPropertyName("userMessage")] + public string UserMessage { get; set; } = string.Empty; +} - /// Session-wide accumulated nano-AI units cost. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { get; set; } +/// Rewind points and file-change-tracking availability for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HistoryListRewindPointsResult +{ + /// Whether this session captured file changes from its first turn. + [JsonPropertyName("fileChangeTrackingEnabled")] + public bool FileChangeTrackingEnabled { get; set; } - /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). - [JsonPropertyName("totalPremiumRequestCost")] - public double TotalPremiumRequestCost { get; set; } + /// Root user turns in chronological order. Empty when `unavailableReason` is set. + [JsonPropertyName("points")] + public IList Points { get => field ??= []; set; } - /// Raw count of user-initiated API requests. - [JsonPropertyName("totalUserRequests")] - public long TotalUserRequests { get; set; } + /// Why the listed points could not be produced, when applicable; the points list is empty whenever it is set. `unsupported-remote-session` is permanent for the session and comes with `fileChangeTrackingEnabled: false`. `session-busy` is transient and only ever reported by a session that *is* tracking (`fileChangeTrackingEnabled: true`), because the file-change captures cannot be read while work that may still mutate them is in flight; the same request succeeds once the session settles, so a client that wants points should retry rather than treat it as a failure. It is never `file-change-tracking-disabled`: an untracked local session still lists conversation-only points and reports that through `fileChangeTrackingEnabled: false`. + [JsonPropertyName("unavailableReason")] + public HistoryRewindUnavailableReason? UnavailableReason { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUsageGetMetricsRequest +internal sealed class SessionHistoryListRewindPointsRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Prediction result. Available results include prediction details; unavailable results include an explicit reason. -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(SessionLimitPredictionResultAvailable), "available")] -[JsonDerivedType(typeof(SessionLimitPredictionResultUnavailable), "unavailable")] -public partial class SessionLimitPredictionResult -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} - - -/// Baseline data provenance for a prediction. +/// A file that a conversation-and-files rewind would restore. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLimitPredictionBaselineData +public sealed class HistoryRewindFilePreview { - /// End of the baseline data slice. - [JsonPropertyName("windowEnd")] - public string WindowEnd { get; set; } = string.Empty; + /// Aggregate change made across the discarded turns. + [JsonPropertyName("changeType")] + public HistoryRewindChangeType ChangeType { get; set; } - /// Start of the baseline data slice. - [JsonPropertyName("windowStart")] - public string WindowStart { get; set; } = string.Empty; -} + /// Lines added across the discarded turns. + [JsonPropertyName("linesAdded")] + public long LinesAdded { get; set; } -/// Semantic usage tier and its AI-credit cap. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLimitPredictionTierOption -{ - /// AI-credit cap for this tier. - [JsonPropertyName("cap")] - public double Cap { get; set; } + /// Lines removed across the discarded turns. + [JsonPropertyName("linesRemoved")] + public long LinesRemoved { get; set; } - /// Semantic usage tier. - [JsonPropertyName("tier")] - public SessionLimitPredictionTier Tier { get; set; } + /// Absolute path of the captured file. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; } -/// Explainable AI-credit session-limit prediction. +/// Files and aggregate changes for a prospective rewind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLimitPredictionDetails +public sealed class HistoryPreviewRewindResult { - /// Baseline data provenance. - [JsonPropertyName("baselineData")] - public SessionLimitPredictionBaselineData BaselineData { get => field ??= new(); set; } - - /// Client population used for the prediction. - [JsonPropertyName("clientType")] - public SessionLimitPredictionClientType ClientType { get; set; } - - /// Resolved model family when known. - [JsonPropertyName("family")] - public string? Family { get; set; } - - /// Model identifier used for lookup. - [JsonPropertyName("modelId")] - public string ModelId { get; set; } = string.Empty; - - /// Recommended maximum AI credits for this session. - [JsonPropertyName("recommendedCap")] - public double RecommendedCap { get; set; } - - /// Tier chosen as the recommended cap. - [JsonPropertyName("recommendedTier")] - public SessionLimitPredictionTier RecommendedTier { get; set; } + /// Whether file restore is available for this session. This is authoritative: switch on it and read `reason` only when it is false. + [JsonPropertyName("available")] + public bool Available { get; set; } - /// Baseline fallback level used to create the prediction. - [JsonPropertyName("source")] - public SessionLimitPredictionSource Source { get; set; } + /// Number of unique files in the preview. + [JsonPropertyName("fileCount")] + public long FileCount { get; set; } - /// Key matched at the source level, such as a model id, family id, or `global`. - [JsonPropertyName("sourceKey")] - public string SourceKey { get; set; } = string.Empty; + /// Files ordered by path. + [JsonPropertyName("files")] + public IList Files { get => field ??= []; set; } - /// Ordered usage tiers and their AI-credit caps. - [JsonPropertyName("tiers")] - public IList Tiers { get => field ??= []; set; } + /// Why file restore is unavailable, when applicable. Populated only when `available` is false and never set when `available` is true. + [JsonPropertyName("reason")] + public HistoryRewindUnavailableReason? Reason { get; set; } } -/// The available variant of . +/// Event boundary to preview for conversation-and-files rewind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionLimitPredictionResultAvailable : SessionLimitPredictionResult +internal sealed class HistoryPreviewRewindRequest { - /// - [JsonIgnore] - public override string Kind => "available"; + /// ID of the user.message event that begins the discarded suffix. + [JsonPropertyName("eventId")] + public string EventId { get; set; } = string.Empty; - /// Predicted session limit details. - [JsonPropertyName("prediction")] - public required SessionLimitPredictionDetails Prediction { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The unavailable variant of . +/// A captured file that rewind intentionally left unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionLimitPredictionResultUnavailable : SessionLimitPredictionResult +public sealed class HistorySkippedFileRestore { - /// - [JsonIgnore] - public override string Kind => "unavailable"; + /// Absolute path of the skipped file. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Reason no prediction is available. + /// Reason the file was not restored. [JsonPropertyName("reason")] - public required SessionLimitPredictionUnavailableReason Reason { get; set; } + public HistoryFileRestoreSkipReason Reason { get; set; } } -/// RPC data type for SessionLimitPredictionPredict operations. +/// Structured outcome of a rewind request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLimitPredictionPredictRequest +public sealed class HistoryRewindResult { - /// Client type to size for. Defaults to `cli-interactive`. - [JsonPropertyName("clientType")] - public SessionLimitPredictionClientType? ClientType { get; set; } + /// Failure detail. Set only for the failure and partial-failure outcomes (`files-rolled-back`, `rollback-incomplete`, `truncation-failed`, `checkpoint-cleanup-failed`, `snapshot-prune-failed`); omitted for `success` and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`). + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Optional model identifier override. If omitted, the session's current model is used. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } -} + /// Number of persisted events removed by conversation truncation. Present only when truncation succeeded (outcomes `success`, `checkpoint-cleanup-failed`, and `snapshot-prune-failed`); omitted for every unavailable outcome (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`) and for `truncation-failed`, `files-rolled-back`, and `rollback-incomplete`. + [JsonPropertyName("eventsRemoved")] + public long? EventsRemoved { get; set; } -/// RPC data type for SessionLimitPredictionPredictRequestWithSession operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionLimitPredictionPredictRequestWithSession -{ - /// Client type to size for. Defaults to `cli-interactive`. - [JsonPropertyName("clientType")] - public SessionLimitPredictionClientType? ClientType { get; set; } + /// Overall rewind outcome. This discriminates the result: it governs which of the remaining fields are populated, so consumers must switch on it before reading `eventsRemoved`, `restoredFiles`, `skippedFiles`, or `error`. See each field for the outcomes that populate it. + [JsonPropertyName("outcome")] + public HistoryRewindOutcome Outcome { get; set; } - /// Optional model identifier override. If omitted, the session's current model is used. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// Absolute paths restored to their captured preimages. Always empty for conversation-only rewinds and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`); only conversation-and-files outcomes that reached the file-restore stage populate it. + [JsonPropertyName("restoredFiles")] + public IList RestoredFiles { get => field ??= []; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Captured files intentionally left unchanged. Always empty for conversation-only rewinds and for the unavailable outcomes (`session-busy`, `file-change-tracking-disabled`, `unsupported-remote-session`); only conversation-and-files outcomes that reached the file-restore stage populate it. + [JsonPropertyName("skippedFiles")] + public IList SkippedFiles { get => field ??= []; set; } } -/// GitHub URL for the session and a flag indicating whether remote steering is enabled. +/// Boundary and mode for rewinding session history. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteEnableResult +internal sealed class HistoryRewindRequest { - /// Whether remote steering is enabled. - [JsonPropertyName("remoteSteerable")] - public bool RemoteSteerable { get; set; } - - /// GitHub frontend URL for this session. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("url")] - public string? Url { get; set; } -} + /// ID of the user.message event that begins the discarded suffix. + [JsonPropertyName("eventId")] + public string EventId { get; set; } = string.Empty; -/// Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class RemoteEnableRequest -{ - /// Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. + /// Whether to rewind only conversation history or also restore captured files. [JsonPropertyName("mode")] - public RemoteSessionMode? Mode { get; set; } + public HistoryRewindMode Mode { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// Indicates whether an in-progress background compaction was cancelled. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HistoryCancelBackgroundCompactionResult +{ + /// Whether an in-progress background compaction was cancelled. False when no compaction was running, when the session is remote, or when the underlying processor was unavailable. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} + /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionRemoteDisableRequest +internal sealed class SessionHistoryCancelBackgroundCompactionRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own. +/// Indicates whether an in-progress manual compaction was aborted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteNotifySteerableChangedResult +public sealed class HistoryAbortManualCompactionResult { + /// Whether an in-progress manual compaction was aborted. False when no manual compaction was running, when its abort controller was already aborted, or when the session is remote. + [JsonPropertyName("aborted")] + public bool Aborted { get; set; } } -/// New remote-steerability state to persist as a `session.remote_steerable_changed` event. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class RemoteNotifySteerableChangedRequest +internal sealed class SessionHistoryAbortManualCompactionRequest { - /// Whether the session now supports remote steering via GitHub. The runtime persists this as a `session.remote_steerable_changed` event so resume/replay sees the up-to-date capability. - [JsonPropertyName("remoteSteerable")] - public bool RemoteSteerable { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Current sharing status and shareable GitHub URL for a session. +/// Markdown summary of the conversation context (empty when not available). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class VisibilityGetResult +public sealed class HistorySummarizeForHandoffResult { - /// Shareable GitHub URL for the session. Present when the session is synced and the URL can be resolved. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("shareUrl")] - public string? ShareUrl { get; set; } - - /// Current sharing status. Absent when the session is not synced or the status could not be retrieved (e.g. the user is not authenticated). - [JsonPropertyName("status")] - public SessionVisibilityStatus? Status { get; set; } - - /// Whether the session has been synced to Mission Control (i.e. has a GitHub task). When false, the session cannot be shared and `status`/`shareUrl` are absent. - [JsonPropertyName("synced")] - public bool Synced { get; set; } + /// Markdown summary of the conversation context produced by an LLM. Empty string when there are no messages or when the session does not support local summarization. + [JsonPropertyName("summary")] + public string Summary { get; set; } = string.Empty; } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionVisibilityGetRequest +internal sealed class SessionHistorySummarizeForHandoffRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Effective sharing status and shareable GitHub URL after updating session visibility. +/// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class VisibilitySetResult +public sealed class HistoryClearContextResult { - /// Shareable GitHub URL for the session. Present when the session is synced and the URL can be resolved. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("shareUrl")] - public string? ShareUrl { get; set; } - - /// Effective sharing status after the update. May differ from the requested status for task types that are already visible to repository readers by default. Absent when the update could not be applied (e.g. the session is not synced or the user is not authenticated). - [JsonPropertyName("status")] - public SessionVisibilityStatus? Status { get; set; } - - /// Whether the session has been synced to Mission Control (i.e. has a GitHub task). When false, the visibility change could not be applied and `status`/`shareUrl` are absent. - [JsonPropertyName("synced")] - public bool Synced { get; set; } + /// Number of non-system, non-developer messages that were removed from the conversation. Zero only when the window already held no conversation. + [JsonPropertyName("messagesCleared")] + public long MessagesCleared { get; set; } } -/// Desired sharing status for the session. +/// Parameters for clearing the conversation and seeding the window that replaces it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class VisibilitySetRequest +internal sealed class HistoryClearContextRequest { + /// First user message of the fresh context window. Required: a cleared window holding only system and developer messages is not a conversation a model can answer, so every clear seeds the window it creates. Delivered by the enclosing turn driver once the agentic loop exits, which is why the call must be made from inside a tool handler. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Sharing status to apply. "repo" makes the session visible to repository readers; "unshared" restricts it to the creator and collaborators. - [JsonPropertyName("status")] - public SessionVisibilityStatus Status { get; set; } } -/// Scheduled prompt entry with ID, timing (`intervalMs`, `cron`, or `at`), prompt text, recurrence, and next run time. +/// User-facing pending queue entry, with kind and display text for a queued message, slash command, or model change. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ScheduleEntry +public sealed class QueuePendingItems { - /// Absolute fire time (epoch milliseconds) for a one-shot calendar schedule. - [JsonPropertyName("at")] - public long? At { get; set; } - - /// 5-field cron expression for a recurring calendar schedule, evaluated in `tz`. - [JsonPropertyName("cron")] - public string? Cron { get; set; } + /// Agent mode stored on this queued entry, as stamped when it was enqueued. Items without an explicit mode report interactive. This is not necessarily the mode that will constrain the turn: a plan or autopilot session applies its own write gate, continuation loop and permission posture to every drained item regardless of the mode stored here. + [JsonPropertyName("agentMode")] + public SendAgentMode AgentMode { get; set; } - /// Display-only label for the prompt as shown in the UI (e.g. `/skill-name` for a skill-invocation schedule). The actual enqueued prompt is `prompt`. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// Human-readable text to display for this queue entry in the UI. + [JsonPropertyName("displayText")] + public string DisplayText { get; set; } = string.Empty; - /// Sequential id assigned by the runtime within the session. Stable across resumes (rebuilt from the event log). + /// Stable opaque id for the canonical queued item. Batch rows share one id. [JsonPropertyName("id")] - public long Id { get; set; } - - /// Interval between scheduled ticks, in milliseconds (relative-interval schedules). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("intervalMs")] - public TimeSpan? Interval { get; set; } - - /// ISO 8601 timestamp when the next tick is scheduled to fire. - [JsonPropertyName("nextRunAt")] - public DateTimeOffset NextRunAt { get; set; } - - /// Prompt text that gets enqueued on every tick. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; - /// Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`). - [JsonPropertyName("recurring")] - public bool Recurring { get; set; } + /// Whether this item is a queued user message or a queued slash command / model change. + [JsonPropertyName("kind")] + public QueuePendingItemsKind Kind { get; set; } - /// True for a self-paced (`dynamic`) schedule: no fixed cadence; the model arms each next run via the `manage_schedule` `wakeup` action. `nextRunAt` is model-controlled. - [JsonPropertyName("selfPaced")] - public bool? SelfPaced { get; set; } + /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } - /// IANA timezone the `cron` expression is evaluated in. - [JsonPropertyName("tz")] - public string? Tz { get; set; } + /// Optional source tag associated with this pending queue entry. This is an open string, not authenticated authorship. In particular, `user` does not prove that a person typed the message. If the source is absent or unrecognized, consumers must not infer human or agent authorship and should handle the entry neutrally. Consumers should tolerate future source values. + [JsonPropertyName("source")] + public string? Source { get; set; } } -/// Snapshot of the currently active recurring prompts for this session. +/// Snapshot of the session's pending queued items and immediate-steering messages. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ScheduleList +public sealed class QueuePendingItemsResult { - /// Active scheduled prompts, ordered by id. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } -} + /// 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; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionScheduleListRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } + + /// Display text for messages currently in the immediate steering queue (interjections sent during a running turn). + [JsonPropertyName("steeringMessages")] + public IList SteeringMessages { get => field ??= []; set; } + + /// ID of the running turn's user message while the model has not answered it, so `withdrawMessage` can still take it back once nothing sent after it is pending. A message leaves `items` when its turn starts, before its `user.message` is recorded; this tells that message apart from one that was removed. Absent when no turn prompt can be taken back. + [JsonPropertyName("withdrawableTurnMessageId")] + public string? WithdrawableTurnMessageId { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionScheduleHydrateRequest +internal sealed class SessionQueuePendingItemsRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Whether the session currently has an active self-paced schedule. +/// Internal snapshot of native queue state for local session orchestration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleHasSelfPacedResult +internal sealed class QueueSnapshotResult { - /// True when at least one active schedule is self-paced. - [JsonPropertyName("hasSelfPaced")] - public bool HasSelfPaced { get; set; } + /// Queue item identifier of a model switch that has been dequeued but not yet applied. + [JsonPropertyName("inFlightModelChangeId")] + public string? InFlightModelChangeId { get; set; } + + /// Insertion orders for queued items, aligned with `items`. + [JsonPropertyName("itemOrders")] + public IList? ItemOrders { get; set; } + + /// User-facing pending items in FIFO order. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } + + /// Insertion orders for immediate steering messages, aligned with `steeringMessages`. + [JsonPropertyName("steeringMessageOrders")] + public IList? SteeringMessageOrders { get; set; } + + /// Immediate steering messages waiting for an active turn. + [JsonPropertyName("steeringMessages")] + public IList SteeringMessages { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionScheduleHasSelfPacedRequest +internal sealed class SessionQueueSnapshotRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of registering or re-arming a scheduled prompt. +/// Result of moving a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddResult +public sealed class QueueMoveItemResult { - /// The registered or updated schedule entry. - [JsonPropertyName("entry")] - public ScheduleEntry? Entry { get; set; } - - /// User-facing validation error, when registration failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// True when the item changed position; false when it was already at the requested position. + [JsonPropertyName("changed")] + public bool Changed { get; set; } } -/// Register a relative-interval scheduled prompt. +/// Parameters for moving a queued item by stable id. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddRequest +internal sealed class QueueMoveItemRequest { - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } - - /// Human-readable interval such as `30s`, `5m`, or `2h`. - [JsonPropertyName("interval")] - public string Interval { get; set; } = string.Empty; - - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; - - /// Whether the schedule should re-arm after each tick. Defaults to true. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } + /// Stable opaque queued-item id. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Zero-based target position in the public visible queue. Values outside the queue clamp to an end. + [JsonPropertyName("toPosition")] + public long ToPosition { get; set; } } -/// Register a cron scheduled prompt. +/// Result of inserting a queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddCronRequest +public sealed class QueueInsertAtResult { - /// 5-field cron expression. - [JsonPropertyName("cron")] - public string Cron { get; set; } = string.Empty; - - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } - - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; - - /// Whether the schedule should re-arm after each tick. Defaults to true. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; - - /// IANA timezone for evaluating the cron expression. - [JsonPropertyName("tz")] - public string? Tz { get; set; } + /// Fresh stable opaque id assigned to the inserted item. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; } -/// Register an absolute-time scheduled prompt. +/// Serializable message fields accepted by queue.insertAt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddAtRequest +public sealed class QueueInsertMessage { - /// Epoch milliseconds when the prompt should fire. - [JsonPropertyName("at")] - public long At { get; set; } - - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// Optional explicit agent mode. When omitted, the session's current mode is assigned. + [JsonPropertyName("agentMode")] + public SendAgentMode? AgentMode { get; set; } - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Optional attachments for the message. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } - /// Whether the schedule should re-arm after each tick. Defaults to false. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } + /// Whether the message is billable. + [JsonPropertyName("billable")] + public bool? Billable { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Accepted for internal SendOptions compatibility but ignored; delivery is derived from current session activity. + [JsonPropertyName("delivery")] + public string? Delivery { get; set; } -/// Register a self-paced scheduled prompt. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddSelfPacedRequest -{ - /// Optional display-only prompt label. + /// Optional user-facing display text. [JsonPropertyName("displayPrompt")] public string? DisplayPrompt { get; set; } - /// Prompt text to enqueue when the schedule fires. + /// Accepted for SendOptions compatibility but ignored; inserted items always use queued delivery semantics. + [JsonPropertyName("mode")] + public SendMode? Mode { get; set; } + + /// Accepted for SendOptions compatibility but ignored; the requested public position controls placement. + [JsonPropertyName("prepend")] + public bool? Prepend { get; set; } + + /// The user message text. [JsonPropertyName("prompt")] public string Prompt { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Per-turn request headers. + [JsonPropertyName("requestHeaders")] + public IDictionary? RequestHeaders { get; set; } + + /// Required tool name for the turn, when any. + [JsonPropertyName("requiredTool")] + public string? RequiredTool { get; set; } + + /// Optional provenance source. `system` is rejected: it would hide the inserted row from `pendingItems` and make it unaddressable while still executing, so inserted items must stay visible. + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// Accepted for SendOptions compatibility but ignored; insertion scheduling is controlled by the queue drain state. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } } -/// Re-arm a self-paced scheduled prompt. +/// Parameters for inserting a queued message at a public visible position. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleRearmSelfPacedRequest +internal sealed class QueueInsertAtRequest { - /// Epoch milliseconds when the prompt should next fire. - [JsonPropertyName("at")] - public long At { get; set; } + /// Queued message contents and delivery metadata. + [JsonPropertyName("message")] + public QueueInsertMessage Message { get => field ??= new(); set; } - /// Id of the self-paced scheduled prompt. - [JsonPropertyName("id")] - public long Id { get; set; } + /// Zero-based position in the public visible queue. Values outside the queue clamp to an end. + [JsonPropertyName("position")] + public long Position { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Remove a scheduled prompt by id. The result entry is omitted if the id was unknown. +/// Result of removing a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ScheduleStopResult +public sealed class QueueRemoveAtResult { - /// The removed entry, or omitted if no entry matched. - [JsonPropertyName("entry")] - public ScheduleEntry? Entry { get; set; } + /// True when the addressed item was removed. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// Identifier of the scheduled prompt to remove. +/// Parameters for removing a queued item by stable id. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleStopRequest +internal sealed class QueueRemoveAtRequest { - /// Id of the scheduled prompt to remove. + /// Stable opaque ID of the queued item to remove. [JsonPropertyName("id")] - public long Id { get; set; } + public string Id { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer <token>` on the outbound request and does no caching; the SDK consumer owns token caching and refresh. +/// Result of editing a queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderTokenAcquireResult +public sealed class QueueUpdateTextResult { - /// The bearer token value (without the `Bearer ` prefix). - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; + /// True when the stored text changed. + [JsonPropertyName("updated")] + public bool Updated { get; set; } } -/// Asks the SDK client to acquire a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request; the runtime does no caching, so this is sent once per request. +/// Parameters for editing a single queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderTokenAcquireRequest +internal sealed class QueueUpdateTextRequest { - /// Name of the BYOK provider needing a token. For the legacy whole-session provider this is the implicit provider name; for named providers it is the configured provider name. - [JsonPropertyName("providerName")] - public string ProviderName { get; set; } = string.Empty; + /// Optional replacement prompt displayed to the user. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Stable opaque ID of the queued item to edit. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Replacement prompt sent to the model. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result returned by an extension workflow closure. +/// Result of withdrawing a user message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowExecuteResult +public sealed class QueueWithdrawMessageResult { - /// Workflow result value. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// True when the running turn was interrupted to withdraw the message. With removed false, the turn was interrupted but its events could not be removed, for example because the model answered first, so the message stays in the interrupted turn. + [JsonPropertyName("interrupted")] + public bool Interrupted { get; set; } + + /// True when the message left the queue or, for a running turn, history. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// Parameters sent to the owning extension to execute a workflow closure. +/// Conditional withdrawal of a single user message, from its queue or from the running turn it started. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowExecuteRequest +internal sealed class QueueWithdrawMessageRequest { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } - - /// Opaque token identifying this workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; - - /// Registered workflow name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// The prompt originally sent. A message edited since submission is not withdrawn, so an obsolete draft cannot replace the edit. + [JsonPropertyName("expectedPrompt")] + public string ExpectedPrompt { get; set; } = string.Empty; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Message identity returned by send, not the queue item id. Batch messages are not eligible. + [JsonPropertyName("messageId")] + public string MessageId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Parameters for cooperatively aborting a workflow body. +/// Append to one pending steering message without changing its identity or delivery position. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAbortRequest +internal sealed class QueueAppendSteeringRequest { - /// Opaque token identifying the execution attempt to abort. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Mode captured at submission. Only steering messages in the same mode may be combined. + [JsonPropertyName("agentMode")] + public SendAgentMode AgentMode { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Attachments to add after the message's existing attachments. An empty list preserves the existing attachments. + [JsonPropertyName("attachments")] + public IList Attachments { get => field ??= []; set; } + + /// Display text to append to the existing preview after a blank line. + [JsonPropertyName("displayPrompt")] + public string DisplayPrompt { get; set; } = string.Empty; + + /// Expected current prompt, including any previous appends. The runtime applies plan-mode normalization before comparing and refuses a changed message. + [JsonPropertyName("expectedPrompt")] + public string ExpectedPrompt { get; set; } = string.Empty; + + /// Message identity returned by send, not the queue item id. Only unclaimed user steering messages are eligible. + [JsonPropertyName("messageId")] + public string MessageId { get; set; } = string.Empty; + + /// Text to append after a blank line. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Whether the client authoritatively confirmed its external work stopped. +/// Result of duplicating a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ClientTaskCancelResult +public sealed class QueueDuplicateAtResult { - /// True only when the owner confirms that external work stopped before responding. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// Fresh stable opaque id assigned to the duplicate. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; } -/// Runtime-to-owner cancellation request for a client-owned task. +/// Parameters for duplicating a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ClientTaskCancelRequest +internal sealed class QueueDuplicateAtRequest { - /// Opaque identifier shared by coalesced cancellation callers. - [JsonPropertyName("cancellationId")] - public string CancellationId { get; set; } = string.Empty; - - /// Owner-scoped task key included for correlation. - [JsonPropertyName("clientTaskId")] - public string ClientTaskId { get; set; } = string.Empty; - - /// Canonical runtime-generated task identifier. + /// Stable opaque ID of the queued item to duplicate. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Reason the runtime requests cancellation. - [JsonPropertyName("reason")] - public ClientTaskCancelReason Reason { get; set; } - - /// Session that owns the client task. + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Describes a filesystem error. +/// Parameters for acquiring or releasing the queued-lane drain pause. Acquisition is exclusive and non-idempotent: `paused: true` against an already-paused session fails with `queue_already_paused`. The pause is never released automatically — it is not tied to the caller's lifetime, so a client that exits without sending `paused: false` leaves the lane frozen. Release is unowned: `paused: false` clears the pause for any caller, including one that never acquired it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsError +internal sealed class QueueSetDrainPausedRequest { - /// Error classification. - [JsonPropertyName("code")] - public SessionFsErrorCode Code { get; set; } + /// Whether queued-lane draining should be paused. + [JsonPropertyName("paused")] + public bool Paused { get; set; } - /// Free-form detail about the error, for logging/diagnostics. - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// File content as a UTF-8 string, or a filesystem error if the read failed. +/// Result of trying to steer a queued message into a live turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReadFileResult +public sealed class QueueSendNowResult { - /// File content as UTF-8 string. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } + /// True when the item was accepted into the steering lane; false when no main turn was live. + [JsonPropertyName("steered")] + public bool Steered { get; set; } } -/// Path of the file to read from the client-provided session filesystem. +/// Parameters for steering a queued message into a live turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReadFileRequest +internal sealed class QueueSendNowRequest { - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Stable opaque ID of the queued item to steer into the live turn. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// File path, content to write, and optional mode for the client-provided session filesystem. +/// Whether the native queue has pending work. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsWriteFileRequest +internal sealed class QueueHasPendingResult { - /// Content to write. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - - /// Optional POSIX-style mode for newly created files. - [JsonPropertyName("mode")] - public long? Mode { get; set; } - - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// True when queued or immediate native work is pending. + [JsonPropertyName("hasPending")] + public bool HasPending { get; set; } } -/// File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsAppendFileRequest +internal sealed class SessionQueueHasPendingRequest { - /// Content to append. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - - /// Optional POSIX-style mode for newly created files. - [JsonPropertyName("mode")] - public long? Mode { get; set; } - - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the requested path exists in the client-provided session filesystem. +/// Whether a deferred-idle drain should run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsExistsResult +internal sealed class QueueBeginDeferredIdleDrainResult { - /// Whether the path exists. - [JsonPropertyName("exists")] - public bool Exists { get; set; } + /// True when the host should run finishDeferredIdleDrain asynchronously. + [JsonPropertyName("shouldDrain")] + public bool ShouldDrain { get; set; } } -/// Path to test for existence in the client-provided session filesystem. +/// Inputs for starting a deferred-idle drain. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsExistsRequest +internal sealed class QueueBeginDeferredIdleDrainRequest { - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Whether the host still has active background work. + [JsonPropertyName("activeBackgroundWork")] + public bool ActiveBackgroundWork { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Filesystem metadata for the requested path, or a filesystem error if the stat failed. +/// Action selected by the native deferred-idle drain. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsStatResult +internal sealed class QueueFinishDeferredIdleDrainResult { - /// ISO 8601 timestamp of creation. - [JsonPropertyName("birthtime")] - public DateTimeOffset Birthtime { get; set; } - - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } - - /// Whether the path is a directory. - [JsonPropertyName("isDirectory")] - public bool IsDirectory { get; set; } - - /// Whether the path is a file. - [JsonPropertyName("isFile")] - public bool IsFile { get; set; } - - /// ISO 8601 timestamp of last modification. - [JsonPropertyName("mtime")] - public DateTimeOffset Mtime { get; set; } + /// Whether the deferred idle was caused by an aborted foreground turn. + [JsonPropertyName("aborted")] + public bool Aborted { get; set; } - /// File size in bytes. - [JsonPropertyName("size")] - public long Size { get; set; } + /// One of none, processQueue, or emitSessionIdle. + [JsonPropertyName("action")] + public string Action { get; set; } = string.Empty; } -/// Path whose metadata should be returned from the client-provided session filesystem. +/// Inputs for completing a deferred-idle drain. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsStatRequest +internal sealed class QueueFinishDeferredIdleDrainRequest { - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Whether the host still has active background work. + [JsonPropertyName("activeBackgroundWork")] + public bool ActiveBackgroundWork { get; set; } + + /// Whether native queued work remains. + [JsonPropertyName("hasPending")] + public bool HasPending { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Directory path to create in the client-provided session filesystem, with options for recursive creation and POSIX mode. +/// Inputs for marking session.idle deferred in native state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsMkdirRequest +internal sealed class QueueDeferSessionIdleRequest { - /// Optional POSIX-style mode for newly created directories. - [JsonPropertyName("mode")] - public long? Mode { get; set; } - - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Create parent directories as needed. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// Whether the deferred idle was caused by an aborted foreground turn. + [JsonPropertyName("aborted")] + public bool Aborted { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Names of entries in the requested directory, or a filesystem error if the read failed. +/// Indicates whether a user-facing pending item was removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirResult +public sealed class QueueRemoveMostRecentResult { - /// Entry names in the directory. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } - - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } + /// True if a user-facing pending item was removed (LIFO across both queues); false when no removable items remained. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// Directory path whose entries should be listed from the client-provided session filesystem. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirRequest +internal sealed class SessionQueueRemoveMostRecentRequest { - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Directory entry returned by session filesystem `readdirWithTypes`, with name and entry type. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirWithTypesEntry +internal sealed class SessionQueueClearRequest { - /// Entry name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Entry type. - [JsonPropertyName("type")] - public SessionFsReaddirWithTypesEntryType Type { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Entries in the requested directory paired with file/directory type information, or a filesystem error if the read failed. +/// Internal filter for consuming queued system notifications. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirWithTypesResult +internal sealed class QueueConsumeSystemNotificationsRequest { - /// Directory entries with type information. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } + /// Opaque runtime-owned filter object. + [JsonPropertyName("filter")] + public JsonElement Filter { get; set; } - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Directory path whose entries (with type information) should be listed from the client-provided session filesystem. +/// Result of enqueueing the resume-pending wake item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirWithTypesRequest +internal sealed class QueueEnqueueResumePendingResult { - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// True when a wake item was newly queued. + [JsonPropertyName("queued")] + public bool Queued { get; set; } } -/// Path to remove from the client-provided session filesystem, with options for recursive removal and force. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsRmRequest +internal sealed class SessionQueueEnqueueResumePendingRequest { - /// Ignore errors if the path does not exist. - [JsonPropertyName("force")] - public bool? Force { get; set; } - - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Remove directories and their contents recursively. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Source and destination paths for renaming or moving an entry in the client-provided session filesystem. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsRenameRequest +internal sealed class SessionQueueProcessRequest { - /// Destination path using SessionFs conventions. - [JsonPropertyName("dest")] - public string Dest { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Source path using SessionFs conventions. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; } -/// Query results including rows, columns, and rows affected, or a filesystem error if execution failed. +/// Cursor, batch size, and optional long-poll/filter parameters for reading session events. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteQueryResult +internal sealed class EventLogReadRequest { - /// Column names from the result set. - [JsonPropertyName("columns")] - public IList Columns { get => field ??= []; set; } - - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } - - /// SQLite last_insert_rowid() value for INSERT. - [JsonPropertyName("lastInsertRowid")] - public long? LastInsertRowid { get; set; } + /// Optional non-empty list of subagent identifiers. When provided, only events owned by one of these agents are returned; ownership recognizes the event envelope's agentId plus legacy data.agentId and data.parentToolCallId markers. This filter takes precedence over agentScope. + [JsonPropertyName("agentIds")] + public IList? AgentIds { get; set; } - /// For SELECT: array of row objects. For others: empty array. - [JsonPropertyName("rows")] - public IList> Rows { get => field ??= []; set; } + /// Agent-scope filter: 'primary' returns only main-agent events plus events whose type starts with 'subagent.' (matching the typed-subscription default behavior); 'all' returns events from all agents (matching wildcard-subscription behavior). Default is 'all' to preserve wildcard semantics for catch-up callers. + [JsonPropertyName("agentScope")] + public EventsAgentScope? AgentScope { get; set; } - /// Number of rows affected (for INSERT/UPDATE/DELETE). - [JsonPropertyName("rowsAffected")] - public long RowsAffected { get; set; } -} + /// Opaque cursor returned by a previous read. Omit on the first call to start from the beginning of the session's persisted history. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } -/// SQL query, query type, and optional bind parameters for executing a SQLite query against the per-session database. The provider applies its SQLite busy timeout for every call. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteQueryRequest -{ - /// Optional named bind parameters. - [JsonPropertyName("params")] - public IDictionary? Params { get; set; } + /// Direction to page through the session's persisted event history. 'forward' (default) pages from the cursor toward newer events (or from the start of history when no cursor is given). 'backward' enables tail-first reads: with no cursor it returns the NEWEST `max` events, and the returned cursor pages toward OLDER events on subsequent backward reads. Events within a returned batch are always in chronological (oldest-to-newest) order, even for a backward read. Backward reads cover PERSISTED history only; ephemeral events are never returned by a backward read. `direction` selects the INITIAL read only: the returned cursor is self-describing, so a continuation read pages in the cursor's own direction regardless of the `direction` passed alongside it — a forward cursor always pages forward and a backward cursor always pages backward. Pass the direction that matches the cursor to avoid confusion. + [JsonPropertyName("direction")] + public EventsReadDirection? Direction { get; set; } - /// SQL query to execute. - [JsonPropertyName("query")] - public string Query { get; set; } = string.Empty; + /// When false, skip ephemeral events entirely and return only durable (persisted) events. History-backfill callers that discard ephemerals anyway should set this so the read is bounded by the durable log length instead of racing the ephemeral ring on a busy session. Defaults to true (ephemerals are interleaved with durable events in creation order). Ignored by backward reads, which always cover persisted history only. + [JsonPropertyName("includeEphemeral")] + public bool? IncludeEphemeral { get; set; } - /// How to execute the query: 'exec' for DDL/multi-statement (no results), 'query' for SELECT (returns rows), 'run' for INSERT/UPDATE/DELETE (returns rowsAffected). - [JsonPropertyName("queryType")] - public SessionFsSqliteQueryType QueryType { get; set; } + /// Maximum number of events to return in this batch (1–1000, default 200). + [JsonPropertyName("max")] + public long? Max { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Either '*' to receive all event types, or a non-empty list of event types to receive. + [JsonPropertyName("types")] + public JsonElement? Types { get; set; } + + /// Milliseconds to wait for new events when the cursor is at the tail of history. 0 (default) returns immediately even if no events are available. Capped at 30000ms. Ephemeral events that arrive during the wait are delivered in this batch but are NOT replayable on a subsequent read (use a non-zero waitMs in your next call to capture future ephemerals as they happen). This applies to forward reads only: a backward read always returns immediately and ignores `waitMs`, because backward paging covers persisted history only while new events append at the tail (the opposite end from a backward page), so no blocking or ephemeral delivery can occur. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("waitMs")] + public TimeSpan? Wait { get; set; } } -/// Classified SQLite transaction failure. busyOrLocked guarantees rollback; postCommitAmbiguous must never be retried. +/// Snapshot of the current tail cursor without returning any events. Use this when a consumer wants to subscribe to live events going forward without first paginating through the entire persisted history (which would happen if `read` were called without a cursor on a long-lived session). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteTransactionError +public sealed class EventLogTailResult { - /// Machine-readable classification of the transaction failure. - [JsonPropertyName("errorClass")] - public SessionFsSqliteTransactionErrorClass ErrorClass { get; set; } - - /// Human-readable transaction failure message. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Opaque cursor pointing at the current tail of the session's persisted-events history. Pass back to `read` to receive only events that arrive AFTER this snapshot. When the session has no events, this returns the same sentinel as an unset cursor (i.e. equivalent to omitting the cursor on a first read). + [JsonPropertyName("cursor")] + public string Cursor { get; set; } = string.Empty; } -/// Per-statement results, or a classified transaction error. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteTransactionResult +internal sealed class SessionEventLogTailRequest { - /// Classified transaction failure, when execution did not succeed. - [JsonPropertyName("error")] - public SessionFsSqliteTransactionError? Error { get; set; } - - /// Per-statement query results in input order. - [JsonPropertyName("results")] - public IList Results { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// One statement in an atomic SQLite transaction. +/// Opaque handle representing an event-type interest registration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteTransactionStatement +public sealed class RegisterEventInterestResult { - /// Optional named bind parameters. - [JsonPropertyName("params")] - public IDictionary? Params { get; set; } - - /// SQL statement to execute. - [JsonPropertyName("query")] - public string Query { get; set; } = string.Empty; - - /// How to execute the statement. - [JsonPropertyName("queryType")] - public SessionFsSqliteQueryType QueryType { get; set; } + /// Opaque handle for this registration. Pass to releaseInterest to release. Each call to registerInterest produces a fresh handle, even when the same eventType is registered multiple times. + [JsonPropertyName("handle")] + public string Handle { get; set; } = string.Empty; } -/// Statements to execute atomically. Providers apply busy handling for every call. +/// Event type to register consumer interest for, used by runtime gating logic. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteTransactionRequest +internal sealed class RegisterEventInterestParams { + /// The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates interactive OAuth token acquisition to the consumer via `mcp.oauth_required` events; when no interest is registered the runtime still attempts non-interactive reconnect from cached or refreshable tokens, and only marks the server `needs-auth` if usable credentials are unavailable — it does not open a browser or start interactive OAuth without a consumer). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `session_limits_exhausted.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. + [JsonPropertyName("eventType")] + public string EventType { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Ordered SQL statements to execute in one transaction. - [JsonPropertyName("statements")] - public IList Statements { get => field ??= []; set; } } -/// Indicates whether the per-session SQLite database already exists. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsSqliteExistsResult +public sealed class EventLogReleaseInterestResult { - /// Whether the session database already exists. - [JsonPropertyName("exists")] - public bool Exists { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Identifies the target session. -public sealed class SessionFsSqliteExistsRequest +/// Opaque handle previously returned by `registerInterest` to release. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ReleaseEventInterestParams { + /// Handle returned by a previous `registerInterest` call. Idempotent: releasing an unknown or already-released handle is a no-op (returns success). When the last outstanding handle for an event type is released, the runtime reverts to its 'no consumer' code path for that event type. + [JsonPropertyName("handle")] + public string Handle { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Canvas open result returned by the provider. +/// Request count and cost metrics for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasProviderOpenResult +public sealed class UsageMetricsModelMetricRequests { - /// Provider-supplied status text. - [JsonPropertyName("status")] - public string? Status { get; set; } - - /// Provider-supplied title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// User-initiated premium request cost (with multiplier applied). + [JsonPropertyName("cost")] + public double Cost { get; set; } - /// URL for web-rendered canvases. - [JsonPropertyName("url")] - public string? Url { get; set; } + /// Number of API requests made with this model. + [JsonPropertyName("count")] + public long Count { get; set; } } -/// Host capabilities. +/// Per-model token-detail entry containing the accumulated token count for one token type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasHostContextCapabilities +public sealed class UsageMetricsModelMetricTokenDetail { - /// Whether canvas rendering is supported. - [JsonPropertyName("canvases")] - public bool? Canvases { get; set; } + /// Accumulated token count for this token type. + [JsonPropertyName("tokenCount")] + public long TokenCount { get; set; } } -/// Host context supplied by the runtime. +/// Token usage metrics for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasHostContext +public sealed class UsageMetricsModelMetricUsage { - /// Host capabilities. - [JsonPropertyName("capabilities")] - public CanvasHostContextCapabilities? Capabilities { get; set; } + /// Total tokens read from prompt cache. + [JsonPropertyName("cacheReadTokens")] + public long CacheReadTokens { get; set; } + + /// Total tokens written to prompt cache. + [JsonPropertyName("cacheWriteTokens")] + public long CacheWriteTokens { get; set; } + + /// Total input tokens consumed. + [JsonPropertyName("inputTokens")] + public long InputTokens { get; set; } + + /// Total output tokens produced. + [JsonPropertyName("outputTokens")] + public long OutputTokens { get; set; } + + /// Total output tokens used for reasoning. + [JsonPropertyName("reasoningTokens")] + public long? ReasoningTokens { get; set; } } -/// Session context supplied by the runtime. +/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasSessionContext +public sealed class UsageMetricsModelMetric { - /// Active session working directory, when known. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } + /// Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. + [JsonPropertyName("cacheExpiresAt")] + public DateTimeOffset? CacheExpiresAt { get; set; } + + /// Request count and cost metrics for this model. + [JsonPropertyName("requests")] + public UsageMetricsModelMetricRequests Requests { get => field ??= new(); set; } + + /// Token count details per type. + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } + + /// Accumulated nano-AI units cost for this model. + [JsonPropertyName("totalNanoAiu")] + public double? TotalNanoAiu { get; set; } + + /// Token usage metrics for this model. + [JsonPropertyName("usage")] + public UsageMetricsModelMetricUsage Usage { get => field ??= new(); set; } } -/// Canvas open parameters sent to the provider. +/// Usage attributed to one agent instance, including its identity, API duration, AI units, and per-model breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasProviderOpenRequest +public sealed class UsageMetricsAgentMetric { - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; - - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; - - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// Human-readable label for this subagent invocation, copied from the originating `subagent.started` event. For task-tool subagents this is the invocation's task description rather than the agent's configured display name, so group by `agentName` for stable per-agent labels. + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } - /// Canvas open input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// Configured agent name, when this is a subagent. + [JsonPropertyName("agentName")] + public string? AgentName { get; set; } - /// Stable caller-supplied canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Per-model usage for this agent, keyed by model identifier. + [JsonPropertyName("modelMetrics")] + public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// Time spent in model API calls by this agent, in milliseconds. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("totalApiDurationMs")] + public TimeSpan TotalApiDuration { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Accumulated nano-AI units cost for this agent. + [JsonPropertyName("totalNanoAiu")] + public double TotalNanoAiu { get; set; } } -/// Canvas close parameters sent to the provider. +/// Aggregated code change metrics. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasProviderCloseRequest +public sealed class UsageMetricsCodeChanges { - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; - - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Distinct file paths modified during the session. + [JsonPropertyName("filesModified")] + public IList FilesModified { get => field ??= []; set; } - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// Number of distinct files modified. + [JsonPropertyName("filesModifiedCount")] + public long FilesModifiedCount { get; set; } - /// Canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Total lines of code added. + [JsonPropertyName("linesAdded")] + public long LinesAdded { get; set; } - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// Total lines of code removed. + [JsonPropertyName("linesRemoved")] + public long LinesRemoved { get; set; } +} - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// Session-wide token-detail entry containing the accumulated token count for one token type. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UsageMetricsTokenDetail +{ + /// Accumulated token count for this token type. + [JsonPropertyName("tokenCount")] + public long TokenCount { get; set; } } -/// Canvas action invocation parameters sent to the provider. +/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasProviderInvokeActionRequest +public sealed class UsageGetMetricsResult { - /// Action name to invoke. - [JsonPropertyName("actionName")] - public string ActionName { get; set; } = string.Empty; + /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. + [JsonPropertyName("agentMetrics")] + public IDictionary? AgentMetrics { get; set; } - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; + /// Aggregated code change metrics. + [JsonPropertyName("codeChanges")] + public UsageMetricsCodeChanges CodeChanges { get => field ??= new(); set; } - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Currently active model identifier. + [JsonPropertyName("currentModel")] + public string? CurrentModel { get; set; } - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// Input tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallInputTokens")] + public long LastCallInputTokens { get; set; } - /// Action input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// Output tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallOutputTokens")] + public long LastCallOutputTokens { get; set; } - /// Canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Per-model token and request metrics, keyed by model identifier. + [JsonPropertyName("modelMetrics")] + public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// ISO 8601 timestamp when the session started. + [JsonPropertyName("sessionStartTime")] + public DateTimeOffset SessionStartTime { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Session-wide per-token-type accumulated token counts. + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } -/// Reports a supervised listener's hosting-task termination and cleanup outcome. -public sealed class HostExitedRequest -{ - /// Explicit startup or teardown failure, when present. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Total time spent in model API calls (milliseconds). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("totalApiDurationMs")] + public TimeSpan TotalApiDuration { get; set; } - /// Process exit status when available; absent for in-process listener tasks. - [JsonPropertyName("exitCode")] - public long? ExitCode { get; set; } + /// Session-wide accumulated nano-AI units cost. + [JsonPropertyName("totalNanoAiu")] + public double? TotalNanoAiu { get; set; } - /// Listener UUID. - [JsonPropertyName("hostId")] - public string HostId { get; set; } = string.Empty; + /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). + [JsonPropertyName("totalPremiumRequestCost")] + public double TotalPremiumRequestCost { get; set; } - /// Cause of termination. - [JsonPropertyName("reason")] - public HostExitReason Reason { get; set; } + /// Raw count of user-initiated API requests. + [JsonPropertyName("totalUserRequests")] + public long TotalUserRequests { get; set; } } -/// Opaque integrator-owned process launch profile for one extension entrypoint. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExtensionLaunchProfile +internal sealed class SessionUsageGetMetricsRequest { - /// Opaque integrator-defined arguments passed to the executable. The runtime does not append the extension entrypoint. - [JsonPropertyName("args")] - public IList Args { get => field ??= []; set; } - - /// Opaque integrator-defined environment variables. Runtime-owned COPILOT_SDK_PATH, SESSION_ID, and COPILOT_EXTENSION_PARENT_PID values take precedence. - [JsonPropertyName("env")] - public IDictionary Env { get => field ??= new Dictionary(); set; } - - /// Executable used to launch the extension entrypoint. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] - [MinLength(1)] - [JsonPropertyName("executable")] - public string Executable { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The launch profile for a supported entrypoint. Omit launch when the provider does not support the entrypoint. +/// Prediction result. Available results include prediction details; unavailable results include an explicit reason. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExtensionLaunchProviderResolveResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SessionLimitPredictionResultAvailable), "available")] +[JsonDerivedType(typeof(SessionLimitPredictionResultUnavailable), "unavailable")] +public partial class SessionLimitPredictionResult { - /// Opaque launch profile, omitted when this provider does not support the entrypoint. - [JsonPropertyName("launch")] - public ExtensionLaunchProfile? Launch { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// A discovered extension entrypoint that the registered integrator may classify and resolve to an opaque launch profile. + +/// Baseline data provenance for a prediction. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExtensionLaunchProviderResolveRequest +public sealed class SessionLimitPredictionBaselineData { - /// Source-qualified extension identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// Absolute path to the discovered extension entrypoint. - [JsonPropertyName("modulePath")] - public string ModulePath { get; set; } = string.Empty; - - /// Human-readable extension name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// End of the baseline data slice. + [JsonPropertyName("windowEnd")] + public string WindowEnd { get; set; } = string.Empty; - /// Discovery source for the extension entrypoint. - [JsonPropertyName("source")] - public ExtensionSource Source { get; set; } + /// Start of the baseline data slice. + [JsonPropertyName("windowStart")] + public string WindowStart { get; set; } = string.Empty; } -/// Acknowledgement. Returning successfully simply means the SDK accepted the start frame; it does not imply the request will succeed. +/// Semantic usage tier and its AI-credit cap. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpRequestStartResult +public sealed class SessionLimitPredictionTierOption { + /// AI-credit cap for this tier. + [JsonPropertyName("cap")] + public double Cap { get; set; } + + /// Semantic usage tier. + [JsonPropertyName("tier")] + public SessionLimitPredictionTier Tier { get; set; } } -/// The head of an outbound model-layer HTTP request. +/// Explainable AI-credit session-limit prediction. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpRequestStartRequest +public sealed class SessionLimitPredictionDetails { - /// Stable identity of the agent trajectory that issued this request. Present when the request originates from an agent turn; absent for requests outside any agent context. This is the same identity used by lifecycle and bridged session events and remains constant across turns and retries. - [JsonPropertyName("agentId")] - public string? AgentId { get; set; } + /// Baseline data provenance. + [JsonPropertyName("baselineData")] + public SessionLimitPredictionBaselineData BaselineData { get => field ??= new(); set; } - /// Identity of the agent invocation (one agentic loop) that issued this request. It remains fixed across physical retries within the invocation and is distinct from the stable trajectory `agentId`. A caller-supplied invocation id always takes precedence (this covers auxiliary calls that have no model call id). Otherwise, first-party CAPI requests fall back to the runtime's agent task id — the same value the runtime emits as the `X-Agent-Task-Id` header — while custom-provider requests fall back to the model call id. - [JsonPropertyName("agentInvocationId")] - public string? AgentInvocationId { get; set; } - - /// HTTP request headers, preserving multiple values per name. - [JsonPropertyName("headers")] - public IDictionary> Headers { get => field ??= new Dictionary>(); set; } + /// Client population used for the prediction. + [JsonPropertyName("clientType")] + public SessionLimitPredictionClientType ClientType { get; set; } - /// Coarse classification of the interaction that produced this request. Open string for forward-compatibility; known values include `conversation-agent`, `conversation-subagent`, `conversation-sampling`, `conversation-background`, `conversation-compaction`, and `conversation-user`. Absent when the runtime did not classify the request. Comes from the runtime's per-request agent context independently of transport; on the CAPI transport the runtime derives the upstream `X-Interaction-Type` header from this same context. - [JsonPropertyName("interactionType")] - public string? InteractionType { get; set; } + /// Resolved model family when known. + [JsonPropertyName("family")] + public string? Family { get; set; } - /// HTTP method, e.g. GET, POST. - [JsonPropertyName("method")] - public string Method { get; set; } = string.Empty; + /// Model identifier used for lookup. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; - /// Stable identity of the immediate parent trajectory. Present for child trajectories such as subagents and conversation-sampling requests; absent for root-agent and non-agent requests. - [JsonPropertyName("parentAgentId")] - public string? ParentAgentId { get; set; } + /// Recommended maximum AI credits for this session. + [JsonPropertyName("recommendedCap")] + public double RecommendedCap { get; set; } - /// Opaque runtime-minted id, unique per in-flight request. The SDK uses this to correlate httpRequestChunk frames and to address its httpResponseStart / httpResponseChunk replies back to the runtime. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Tier chosen as the recommended cap. + [JsonPropertyName("recommendedTier")] + public SessionLimitPredictionTier RecommendedTier { get; set; } - /// Id of the runtime session that triggered this request, when one is in scope. Absent for requests issued outside any session (e.g. startup model-catalog or capability resolution). This is a payload field — not a dispatch key — because the client-global API is registered process-wide rather than per session. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + /// Baseline fallback level used to create the prediction. + [JsonPropertyName("source")] + public SessionLimitPredictionSource Source { get; set; } - /// Transport the runtime would otherwise use for this request. `http` (the default when absent) covers plain HTTP and SSE responses; `websocket` indicates a full-duplex message channel where each body chunk maps to one WebSocket message and the `binary` flag distinguishes text from binary frames. The SDK consumer uses this to decide whether to service the request with an HTTP client or a WebSocket client. It is the one piece of request metadata the consumer cannot reliably infer from the URL or headers alone. - [JsonPropertyName("transport")] - public LlmInferenceHttpRequestStartTransport? Transport { get; set; } + /// Key matched at the source level, such as a model id, family id, or `global`. + [JsonPropertyName("sourceKey")] + public string SourceKey { get; set; } = string.Empty; - /// Absolute request URL. - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; + /// Ordered usage tiers and their AI-credit caps. + [JsonPropertyName("tiers")] + public IList Tiers { get => field ??= []; set; } } -/// Acknowledgement. The SDK is free to ignore the ack and treat chunk delivery as fire-and-forget. +/// The available variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpRequestChunkResult +public partial class SessionLimitPredictionResultAvailable : SessionLimitPredictionResult { + /// + [JsonIgnore] + public override string Kind => "available"; + + /// Predicted session limit details. + [JsonPropertyName("prediction")] + public required SessionLimitPredictionDetails Prediction { get; set; } } -/// A request body chunk or cancellation signal. +/// The unavailable variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class LlmInferenceHttpRequestChunkRequest +public partial class SessionLimitPredictionResultUnavailable : SessionLimitPredictionResult { - /// Identity of the agent invocation (one agentic loop) this body chunk belongs to, matching the `agentInvocationId` semantics on httpRequestStart. Carried per chunk so a persistent transport can attribute successive turns correctly: when a WebSocket connection is reused across turns, the httpRequestStart identity reflects only the turn that opened the connection, so each later turn stamps its own invocation id here. Absent when the runtime has no invocation context for the request, or on the plain-HTTP transport where every request has its own httpRequestStart. - [JsonPropertyName("agentInvocationId")] - public string? AgentInvocationId { get; set; } - - /// When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text. - [JsonPropertyName("binary")] - public bool? Binary { get; set; } - - /// When true, the runtime is cancelling the in-flight request (e.g. upstream consumer aborted). `data` is ignored. Implies end-of-request. - [JsonPropertyName("cancel")] - public bool? Cancel { get; set; } - - /// Optional human-readable reason for the cancellation, propagated for logging. - [JsonPropertyName("cancelReason")] - public string? CancelReason { get; set; } - - /// Body byte range. UTF-8 text when `binary` is absent or false; base64-encoded bytes when `binary` is true. May be empty. - [JsonPropertyName("data")] - public string Data { get; set; } = string.Empty; - - /// When true, this is the final body chunk for the request. The SDK may rely on having received an end-marked chunk before treating the request body as complete. - [JsonPropertyName("end")] - public bool? End { get; set; } + /// + [JsonIgnore] + public override string Kind => "unavailable"; - /// Matches the requestId from the originating httpRequestStart frame. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Reason no prediction is available. + [JsonPropertyName("reason")] + public required SessionLimitPredictionUnavailableReason Reason { get; set; } } -/// Client environment metadata describing the process that produced a telemetry event. +/// RPC data type for SessionLimitPredictionPredict operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryClientInfo +public sealed class SessionLimitPredictionPredictRequest { - /// Copilot CLI version string. - [JsonPropertyName("cli_version")] - public string CliVersion { get; set; } = string.Empty; - - /// Name of the client application. - [JsonPropertyName("client_name")] - public string? ClientName { get; set; } - - /// Type of client. - [JsonPropertyName("client_type")] - public string? ClientType { get; set; } - - /// Copilot subscription plan, when known. - [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; } + /// Client type to size for. Defaults to `cli-interactive`. + [JsonPropertyName("clientType")] + public SessionLimitPredictionClientType? ClientType { get; set; } - /// Stable machine identifier for the device. - [JsonPropertyName("dev_device_id")] - public string? DevDeviceId { get; set; } + /// Optional model identifier override. If omitted, the session's current model is used. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } +} - /// Whether the user is a GitHub/Microsoft staff member. - [JsonPropertyName("is_staff")] - public bool? IsStaff { get; set; } +/// RPC data type for SessionLimitPredictionPredictRequestWithSession operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionLimitPredictionPredictRequestWithSession +{ + /// Client type to size for. Defaults to `cli-interactive`. + [JsonPropertyName("clientType")] + public SessionLimitPredictionClientType? ClientType { get; set; } - /// Node.js runtime version string. - [JsonPropertyName("node_version")] - public string NodeVersion { get; set; } = string.Empty; + /// Optional model identifier override. If omitted, the session's current model is used. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } - /// Operating system architecture (e.g. arm64, x64). - [JsonPropertyName("os_arch")] - public string OsArch { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Operating system platform (e.g. darwin, linux, win32). - [JsonPropertyName("os_platform")] - public string OsPlatform { get; set; } = string.Empty; +/// GitHub URL for the session and a flag indicating whether remote steering is enabled. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class RemoteEnableResult +{ + /// Whether remote steering is enabled. + [JsonPropertyName("remoteSteerable")] + public bool RemoteSteerable { get; set; } - /// Operating system version string. - [JsonPropertyName("os_version")] - public string OsVersion { get; set; } = string.Empty; + /// GitHub frontend URL for this session. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// A single telemetry event in the runtime's native GitHub-shaped telemetry format, forwarded verbatim to opted-in hosts. The `restricted` flag on the enclosing GitHubTelemetryNotification distinguishes standard from restricted events; the payload shape is identical for both. +/// Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryEvent +internal sealed class RemoteEnableRequest { - /// Client environment metadata. - [JsonPropertyName("client")] - public GitHubTelemetryClientInfo? Client { get; set; } + /// Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. + [JsonPropertyName("mode")] + public RemoteSessionMode? Mode { get; set; } - /// Copilot tracking ID for user-level attribution. - [JsonPropertyName("copilot_tracking_id")] - public string? CopilotTrackingId { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Timestamp when the event was created (ISO 8601 format). - [JsonPropertyName("created_at")] - public string? CreatedAt { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionRemoteDisableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Experiment assignment context. - [JsonPropertyName("exp_assignment_context")] - public string? ExpAssignmentContext { get; set; } +/// Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class RemoteNotifySteerableChangedResult +{ +} - /// Feature flags enabled for this session, as a map from flag to value. - [JsonPropertyName("features")] - public IDictionary? Features { get; set; } +/// New remote-steerability state to persist as a `session.remote_steerable_changed` event. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class RemoteNotifySteerableChangedRequest +{ + /// Whether the session now supports remote steering via GitHub. The runtime persists this as a `session.remote_steerable_changed` event so resume/replay sees the up-to-date capability. + [JsonPropertyName("remoteSteerable")] + public bool RemoteSteerable { get; set; } - /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). - [JsonPropertyName("kind")] - public string Kind { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Numeric metrics as a map from key to value. - [JsonPropertyName("metrics")] - public IDictionary Metrics { get => field ??= new Dictionary(); set; } +/// Current sharing status and shareable GitHub URL for a session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class VisibilityGetResult +{ + /// Shareable GitHub URL for the session. Present when the session is synced and the URL can be resolved. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("shareUrl")] + public string? ShareUrl { get; set; } - /// Reference to the model call that produced this event. - [JsonPropertyName("model_call_id")] - public string? ModelCallId { get; set; } + /// Current sharing status. Absent when the session is not synced or the status could not be retrieved (e.g. the user is not authenticated). + [JsonPropertyName("status")] + public SessionVisibilityStatus? Status { get; set; } - /// String-valued properties as a map from key to value. - [JsonPropertyName("properties")] - public IDictionary Properties { get => field ??= new Dictionary(); set; } + /// Whether the session has been synced to Mission Control (i.e. has a GitHub task). When false, the session cannot be shared and `status`/`shareUrl` are absent. + [JsonPropertyName("synced")] + public bool Synced { get; set; } +} - /// Session identifier the event belongs to. - [JsonPropertyName("session_id")] - public string? SessionId { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionVisibilityGetRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Payload for a `gitHubTelemetry.event` notification: a single GitHub telemetry event the runtime forwards to a host connection that opted into telemetry forwarding during the `server.connect` handshake. +/// Effective sharing status and shareable GitHub URL after updating session visibility. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryNotification +public sealed class VisibilitySetResult { - /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. - [JsonPropertyName("event")] - public GitHubTelemetryEvent Event { get => field ??= new(); set; } + /// Shareable GitHub URL for the session. Present when the session is synced and the URL can be resolved. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("shareUrl")] + public string? ShareUrl { get; set; } - /// Whether this is a restricted telemetry event (cli.restricted_telemetry). Hosts must route restricted events to first-party Microsoft stores only. - [JsonPropertyName("restricted")] - public bool Restricted { get; set; } + /// Effective sharing status after the update. May differ from the requested status for task types that are already visible to repository readers by default. Absent when the update could not be applied (e.g. the session is not synced or the user is not authenticated). + [JsonPropertyName("status")] + public SessionVisibilityStatus? Status { get; set; } - /// Session the telemetry event belongs to, when it is session-scoped. Omitted for sessionless events (for example, `server.sendTelemetry` calls with no session id), which are still forwarded to opted-in connections. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + /// Whether the session has been synced to Mission Control (i.e. has a GitHub task). When false, the visibility change could not be applied and `status`/`shareUrl` are absent. + [JsonPropertyName("synced")] + public bool Synced { get; set; } } -/// SDK host response to a GitHub credential request. -/// Polymorphic base type discriminated by kind. +/// Desired sharing status for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(GitHubTokenAcquireResultToken), "token")] -[JsonDerivedType(typeof(GitHubTokenAcquireResultCancelled), "cancelled")] -public partial class GitHubTokenAcquireResult +internal sealed class VisibilitySetRequest { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + /// Sharing status to apply. "repo" makes the session visible to repository readers; "unshared" restricts it to the creator and collaborators. + [JsonPropertyName("status")] + public SessionVisibilityStatus Status { get; set; } +} -/// The token variant of . +/// Scheduled prompt entry with ID, timing (`intervalMs`, `cron`, or `at`), prompt text, recurrence, and next run time. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class GitHubTokenAcquireResultToken : GitHubTokenAcquireResult +public sealed class ScheduleEntry { - /// - [JsonIgnore] - public override string Kind => "token"; + /// Absolute fire time (epoch milliseconds) for a one-shot calendar schedule. + [JsonPropertyName("at")] + public long? At { get; set; } - /// GitHub access token acquired by the SDK host. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } + /// 5-field cron expression for a recurring calendar schedule, evaluated in `tz`. + [JsonPropertyName("cron")] + public string? Cron { 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; } + /// Display-only label for the prompt as shown in the UI (e.g. `/skill-name` for a skill-invocation schedule). The actual enqueued prompt is `prompt`. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } - /// OAuth token type. Defaults to bearer when omitted. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("tokenType")] - public string? TokenType { get; set; } + /// Sequential id assigned by the runtime within the session. Stable across resumes (rebuilt from the event log). + [JsonPropertyName("id")] + public long Id { get; set; } + + /// Interval between scheduled ticks, in milliseconds (relative-interval schedules). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("intervalMs")] + public TimeSpan? Interval { get; set; } + + /// ISO 8601 timestamp when the next tick is scheduled to fire. + [JsonPropertyName("nextRunAt")] + public DateTimeOffset NextRunAt { get; set; } + + /// Prompt text that gets enqueued on every tick. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`). + [JsonPropertyName("recurring")] + public bool Recurring { get; set; } + + /// True for a self-paced (`dynamic`) schedule: no fixed cadence; the model arms each next run via the `manage_schedule` `wakeup` action. `nextRunAt` is model-controlled. + [JsonPropertyName("selfPaced")] + public bool? SelfPaced { get; set; } + + /// IANA timezone the `cron` expression is evaluated in. + [JsonPropertyName("tz")] + public string? Tz { get; set; } } -/// The cancelled variant of . +/// Snapshot of the currently active recurring prompts for this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class GitHubTokenAcquireResultCancelled : GitHubTokenAcquireResult +public sealed class ScheduleList { - /// - [JsonIgnore] - public override string Kind => "cancelled"; + /// Active scheduled prompts, ordered by id. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } } -/// Asks the SDK client to acquire a GitHub access token from an opaque callback registration. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTokenAcquireRequest +internal sealed class SessionScheduleListRequest { - /// 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. + /// Target session identifier. [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } + public string SessionId { get; set; } = string.Empty; } -/// A response is meaningful only on the connection and request that issued its challenge. -public sealed class InstallationsConfirmResult +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionScheduleHydrateRequest { - /// Exact challenge from the request. - [JsonPropertyName("confirmationId")] - public string ConfirmationId { get; set; } = string.Empty; - - /// Fresh explicit user decision. There is no default. - [JsonPropertyName("decision")] - public InstallationDecision Decision { get; set; } - - /// Exact review commitment from the request. - [JsonPropertyName("reviewFingerprint")] - public string ReviewFingerprint { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Only resource kinds with an implemented installation engine have a review variant. -/// Polymorphic base type discriminated by resource. +/// Whether the session currently has an active self-paced schedule. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "resource", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(InstallationReviewMcp), "mcp")] -[JsonDerivedType(typeof(InstallationReviewSkill), "skill")] -public partial class InstallationReview +internal sealed class ScheduleHasSelfPacedResult { - /// The type discriminator. - [JsonPropertyName("resource")] - public virtual string Resource { get; set; } = string.Empty; + /// True when at least one active schedule is self-paced. + [JsonPropertyName("hasSelfPaced")] + public bool HasSelfPaced { get; set; } } - -/// Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. -/// Polymorphic base type discriminated by action. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "action", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(McpInstallationReviewInstall), "install")] -[JsonDerivedType(typeof(McpInstallationReviewUninstall), "uninstall")] -public partial class McpInstallationReview +internal sealed class SessionScheduleHasSelfPacedRequest { - /// The type discriminator. - [JsonPropertyName("action")] - public virtual string Action { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } +/// Result of registering or re-arming a scheduled prompt. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ScheduleAddResult +{ + /// The registered or updated schedule entry. + [JsonPropertyName("entry")] + public ScheduleEntry? Entry { get; set; } + + /// User-facing validation error, when registration failed. + [JsonPropertyName("error")] + public string? Error { get; set; } +} -/// -/// Final remote configuration, not a template. The producer refuses external-value -/// expansion before presenting this review. Receipt-owned secrets appear only as -/// `${installation-secret:<id>}` references whose `<id>` matches a reviewed -/// `${secret:<id>}` placeholder; values are never included. -/// +/// Register a relative-interval scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpInstallationRemoteConfiguration +internal sealed class ScheduleAddRequest { - /// - /// Configured headers, excluding separately authorised OAuth tokens. Values may - /// contain owned secret references, never secret values. - /// - [JsonPropertyName("headers")] - public IDictionary Headers { get => field ??= new Dictionary(); set; } + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } - /// Configured tool selection, not permission to invoke those tools. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } + /// Human-readable interval such as `30s`, `5m`, or `2h`. + [JsonPropertyName("interval")] + public string Interval { get; set; } = string.Empty; - /// Transport in the effective persisted remote configuration. - [JsonPropertyName("transport")] - public McpPlanRemoteTransport Transport { get; set; } + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Exact resolved endpoint, without templates or secret placeholders. - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; + /// Whether the schedule should re-arm after each tick. Defaults to true. + [JsonPropertyName("recurring")] + public bool? Recurring { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The install variant of . +/// Register a cron scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpInstallationReviewInstall : McpInstallationReview +internal sealed class ScheduleAddCronRequest { - /// - [JsonIgnore] - public override string Action => "install"; + /// 5-field cron expression. + [JsonPropertyName("cron")] + public string Cron { get; set; } = string.Empty; - /// Catalogue identity retained from the bound candidate when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("catalogue")] - public InstallationCatalogueIdentity? Catalogue { get; set; } + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// Whether the schedule should re-arm after each tick. Defaults to true. + [JsonPropertyName("recurring")] + public bool? Recurring { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// IANA timezone for evaluating the cron expression. + [JsonPropertyName("tz")] + public string? Tz { get; set; } +} + +/// Register an absolute-time scheduled prompt. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ScheduleAddAtRequest +{ + /// Epoch milliseconds when the prompt should fire. + [JsonPropertyName("at")] + public long At { get; set; } + + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// Whether the schedule should re-arm after each tick. Defaults to false. + [JsonPropertyName("recurring")] + public bool? Recurring { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Register a self-paced scheduled prompt. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ScheduleAddSelfPacedRequest +{ + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } + + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Re-arm a self-paced scheduled prompt. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ScheduleRearmSelfPacedRequest +{ + /// Epoch milliseconds when the prompt should next fire. + [JsonPropertyName("at")] + public long At { get; set; } + + /// Id of the self-paced scheduled prompt. + [JsonPropertyName("id")] + public long Id { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Remove a scheduled prompt by id. The result entry is omitted if the id was unknown. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ScheduleStopResult +{ + /// The removed entry, or omitted if no entry matched. + [JsonPropertyName("entry")] + public ScheduleEntry? Entry { get; set; } +} + +/// Identifier of the scheduled prompt to remove. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ScheduleStopRequest +{ + /// Id of the scheduled prompt to remove. + [JsonPropertyName("id")] + public long Id { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer <token>` on the outbound request and does no caching; the SDK consumer owns token caching and refresh. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderTokenAcquireResult +{ + /// The bearer token value (without the `Bearer ` prefix). + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; +} + +/// Asks the SDK client to acquire a bearer token for a BYOK provider whose config set `hasBearerTokenProvider: true`. Issued by the runtime before each outbound model request; the runtime does no caching, so this is sent once per request. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderTokenAcquireRequest +{ + /// Name of the BYOK provider needing a token. For the legacy whole-session provider this is the implicit provider name; for named providers it is the configured provider name. + [JsonPropertyName("providerName")] + public string ProviderName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Result returned by an extension workflow closure. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowExecuteResult +{ + /// Workflow result value. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} + +/// Parameters sent to the owning extension to execute a workflow closure. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowExecuteRequest +{ + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } + + /// Opaque token identifying this workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; + + /// Registered workflow name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Parameters for cooperatively aborting a workflow body. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowAbortRequest +{ + /// Opaque token identifying the execution attempt to abort. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; + + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Whether the client authoritatively confirmed its external work stopped. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ClientTaskCancelResult +{ + /// True only when the owner confirms that external work stopped before responding. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} + +/// Runtime-to-owner cancellation request for a client-owned task. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ClientTaskCancelRequest +{ + /// Opaque identifier shared by coalesced cancellation callers. + [JsonPropertyName("cancellationId")] + public string CancellationId { get; set; } = string.Empty; + + /// Owner-scoped task key included for correlation. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Reason the runtime requests cancellation. + [JsonPropertyName("reason")] + public ClientTaskCancelReason Reason { get; set; } + + /// Session that owns the client task. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Describes a filesystem error. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsError +{ + /// Error classification. + [JsonPropertyName("code")] + public SessionFsErrorCode Code { get; set; } + + /// Free-form detail about the error, for logging/diagnostics. + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + [JsonPropertyName("writeChanged")] + public bool? WriteChanged { get; set; } +} + +/// File content as a UTF-8 string, or a filesystem error if the read failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReadFileResult +{ + /// File content as UTF-8 string. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } +} + +/// Path of the file to read from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReadFileRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// File bytes as standard base64, or a filesystem error if the read failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReadFileBytesResult +{ + /// Exact file bytes encoded as standard base64. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } +} + +/// Path of the binary file to read from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReadFileBytesRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// File path, content to write, and optional mode for the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsWriteFileRequest +{ + /// Content to write. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Optional POSIX-style mode for newly created files. + [JsonPropertyName("mode")] + public long? Mode { get; set; } + + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsWriteFileBytesRequest +{ + /// Exact file bytes encoded as standard base64. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Optional POSIX-style mode for newly created files. + [JsonPropertyName("mode")] + public long? Mode { get; set; } + + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsAppendFileRequest +{ + /// Content to append. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + + /// Optional POSIX-style mode for newly created files. + [JsonPropertyName("mode")] + public long? Mode { get; set; } + + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Indicates whether the requested path exists in the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsExistsResult +{ + /// Whether the path exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } +} + +/// Path to test for existence in the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsExistsRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Filesystem metadata for the requested path, or a filesystem error if the stat failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsStatResult +{ + /// ISO 8601 timestamp of creation. + [JsonPropertyName("birthtime")] + public DateTimeOffset Birthtime { get; set; } + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } + + /// Whether the path is a directory. + [JsonPropertyName("isDirectory")] + public bool IsDirectory { get; set; } + + /// Whether the path is a file. + [JsonPropertyName("isFile")] + public bool IsFile { get; set; } + + /// ISO 8601 timestamp of last modification. + [JsonPropertyName("mtime")] + public DateTimeOffset Mtime { get; set; } + + /// File size in bytes. + [JsonPropertyName("size")] + public long Size { get; set; } +} + +/// Path whose metadata should be returned from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsStatRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Directory path to create in the client-provided session filesystem, with options for recursive creation and POSIX mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsMkdirRequest +{ + /// Optional POSIX-style mode for newly created directories. + [JsonPropertyName("mode")] + public long? Mode { get; set; } + + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Create parent directories as needed. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Names of entries in the requested directory, or a filesystem error if the read failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReaddirResult +{ + /// Entry names in the directory. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } +} + +/// Directory path whose entries should be listed from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReaddirRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Directory entry returned by session filesystem `readdirWithTypes`, with name and entry type. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReaddirWithTypesEntry +{ + /// Entry name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Entry type. + [JsonPropertyName("type")] + public SessionFsReaddirWithTypesEntryType Type { get; set; } +} + +/// Entries in the requested directory paired with file/directory type information, or a filesystem error if the read failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReaddirWithTypesResult +{ + /// Directory entries with type information. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } +} + +/// Directory path whose entries (with type information) should be listed from the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsReaddirWithTypesRequest +{ + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Path to remove from the client-provided session filesystem, with options for recursive removal and force. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsRmRequest +{ + /// Ignore errors if the path does not exist. + [JsonPropertyName("force")] + public bool? Force { get; set; } + + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Remove directories and their contents recursively. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Source and destination paths for renaming or moving an entry in the client-provided session filesystem. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsRenameRequest +{ + /// Destination path using SessionFs conventions. + [JsonPropertyName("dest")] + public string Dest { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Source path using SessionFs conventions. + [JsonPropertyName("src")] + public string Src { get; set; } = string.Empty; +} + +/// Query results including rows, columns, and rows affected, or a filesystem error if execution failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsSqliteQueryResult +{ + /// Column names from the result set. + [JsonPropertyName("columns")] + public IList Columns { get => field ??= []; set; } + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } + + /// SQLite last_insert_rowid() value for INSERT. + [JsonPropertyName("lastInsertRowid")] + public long? LastInsertRowid { get; set; } + + /// For SELECT: array of row objects. For others: empty array. + [JsonPropertyName("rows")] + public IList> Rows { get => field ??= []; set; } + + /// Number of rows affected (for INSERT/UPDATE/DELETE). + [JsonPropertyName("rowsAffected")] + public long RowsAffected { get; set; } +} + +/// SQL query, query type, and optional bind parameters for executing a SQLite query against the per-session database. The provider applies its SQLite busy timeout for every call. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsSqliteQueryRequest +{ + /// Optional named bind parameters. + [JsonPropertyName("params")] + public IDictionary? Params { get; set; } + + /// SQL query to execute. + [JsonPropertyName("query")] + public string Query { get; set; } = string.Empty; + + /// How to execute the query: 'exec' for DDL/multi-statement (no results), 'query' for SELECT (returns rows), 'run' for INSERT/UPDATE/DELETE (returns rowsAffected). + [JsonPropertyName("queryType")] + public SessionFsSqliteQueryType QueryType { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Classified SQLite transaction failure. busyOrLocked guarantees rollback; postCommitAmbiguous must never be retried. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsSqliteTransactionError +{ + /// Machine-readable classification of the transaction failure. + [JsonPropertyName("errorClass")] + public SessionFsSqliteTransactionErrorClass ErrorClass { get; set; } + + /// Human-readable transaction failure message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; +} + +/// Per-statement results, or a classified transaction error. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsSqliteTransactionResult +{ + /// Classified transaction failure, when execution did not succeed. + [JsonPropertyName("error")] + public SessionFsSqliteTransactionError? Error { get; set; } + + /// Per-statement query results in input order. + [JsonPropertyName("results")] + public IList Results { get => field ??= []; set; } +} + +/// One statement in an atomic SQLite transaction. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsSqliteTransactionStatement +{ + /// Optional named bind parameters. + [JsonPropertyName("params")] + public IDictionary? Params { get; set; } + + /// SQL statement to execute. + [JsonPropertyName("query")] + public string Query { get; set; } = string.Empty; + + /// How to execute the statement. + [JsonPropertyName("queryType")] + public SessionFsSqliteQueryType QueryType { get; set; } +} + +/// Statements to execute atomically. Providers apply busy handling for every call. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsSqliteTransactionRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Ordered SQL statements to execute in one transaction. + [JsonPropertyName("statements")] + public IList Statements { get => field ??= []; set; } +} + +/// Indicates whether the per-session SQLite database already exists. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionFsSqliteExistsResult +{ + /// Whether the session database already exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } +} + +/// Identifies the target session. +public sealed class SessionFsSqliteExistsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Canvas open result returned by the provider. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasProviderOpenResult +{ + /// Provider-supplied status text. + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// Provider-supplied title. + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// URL for web-rendered canvases. + [JsonPropertyName("url")] + public string? Url { get; set; } +} + +/// Host capabilities. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasHostContextCapabilities +{ + /// Whether canvas rendering is supported. + [JsonPropertyName("canvases")] + public bool? Canvases { get; set; } +} + +/// Host context supplied by the runtime. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasHostContext +{ + /// Host capabilities. + [JsonPropertyName("capabilities")] + public CanvasHostContextCapabilities? Capabilities { get; set; } +} + +/// Session context supplied by the runtime. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasSessionContext +{ + /// Active session working directory, when known. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } +} + +/// Canvas open parameters sent to the provider. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasProviderOpenRequest +{ + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; + + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; + + /// Host context supplied by the runtime. + [JsonPropertyName("host")] + public CanvasHostContext? Host { get; set; } + + /// Canvas open input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } + + /// Stable caller-supplied canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Session context supplied by the runtime. + [JsonPropertyName("session")] + public CanvasSessionContext? Session { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Canvas close parameters sent to the provider. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasProviderCloseRequest +{ + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; + + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; + + /// Host context supplied by the runtime. + [JsonPropertyName("host")] + public CanvasHostContext? Host { get; set; } + + /// Canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Session context supplied by the runtime. + [JsonPropertyName("session")] + public CanvasSessionContext? Session { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Canvas action invocation parameters sent to the provider. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CanvasProviderInvokeActionRequest +{ + /// Action name to invoke. + [JsonPropertyName("actionName")] + public string ActionName { get; set; } = string.Empty; + + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; + + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; + + /// Host context supplied by the runtime. + [JsonPropertyName("host")] + public CanvasHostContext? Host { get; set; } + + /// Action input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } + + /// Canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Session context supplied by the runtime. + [JsonPropertyName("session")] + public CanvasSessionContext? Session { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Reports a supervised listener's hosting-task termination and cleanup outcome. +public sealed class HostExitedRequest +{ + /// Explicit startup or teardown failure, when present. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Process exit status when available; absent for in-process listener tasks. + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Listener UUID. + [JsonPropertyName("hostId")] + public string HostId { get; set; } = string.Empty; + + /// Cause of termination. + [JsonPropertyName("reason")] + public HostExitReason Reason { get; set; } +} + +/// Opaque integrator-owned process launch profile for one extension entrypoint. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ExtensionLaunchProfile +{ + /// Opaque integrator-defined arguments passed to the executable. The runtime does not append the extension entrypoint. + [JsonPropertyName("args")] + public IList Args { get => field ??= []; set; } + + /// Opaque integrator-defined environment variables. Runtime-owned COPILOT_SDK_PATH, SESSION_ID, and COPILOT_EXTENSION_PARENT_PID values take precedence. + [JsonPropertyName("env")] + public IDictionary Env { get => field ??= new Dictionary(); set; } + + /// Executable used to launch the extension entrypoint. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("executable")] + public string Executable { get; set; } = string.Empty; +} + +/// The launch profile for a supported entrypoint. Omit launch when the provider does not support the entrypoint. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ExtensionLaunchProviderResolveResult +{ + /// Opaque launch profile, omitted when this provider does not support the entrypoint. + [JsonPropertyName("launch")] + public ExtensionLaunchProfile? Launch { get; set; } +} + +/// A discovered extension entrypoint that the registered integrator may classify and resolve to an opaque launch profile. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ExtensionLaunchProviderResolveRequest +{ + /// Source-qualified extension identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Absolute path to the discovered extension entrypoint. + [JsonPropertyName("modulePath")] + public string ModulePath { get; set; } = string.Empty; + + /// Human-readable extension name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Discovery source for the extension entrypoint. + [JsonPropertyName("source")] + public ExtensionSource Source { get; set; } +} + +/// Acknowledgement. Returning successfully simply means the SDK accepted the start frame; it does not imply the request will succeed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class LlmInferenceHttpRequestStartResult +{ +} + +/// The head of an outbound model-layer HTTP request. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class LlmInferenceHttpRequestStartRequest +{ + /// Stable identity of the agent trajectory that issued this request. Present when the request originates from an agent turn; absent for requests outside any agent context. This is the same identity used by lifecycle and bridged session events and remains constant across turns and retries. + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } + + /// Identity of the agent invocation (one agentic loop) that issued this request. It remains fixed across physical retries within the invocation and is distinct from the stable trajectory `agentId`. A caller-supplied invocation id always takes precedence (this covers auxiliary calls that have no model call id). Otherwise, first-party CAPI requests fall back to the runtime's agent task id — the same value the runtime emits as the `X-Agent-Task-Id` header — while custom-provider requests fall back to the model call id. + [JsonPropertyName("agentInvocationId")] + public string? AgentInvocationId { get; set; } + + /// HTTP request headers, preserving multiple values per name. + [JsonPropertyName("headers")] + public IDictionary> Headers { get => field ??= new Dictionary>(); set; } + + /// Coarse classification of the interaction that produced this request. Open string for forward-compatibility; known values include `conversation-agent`, `conversation-subagent`, `conversation-sampling`, `conversation-background`, `conversation-compaction`, and `conversation-user`. Absent when the runtime did not classify the request. Comes from the runtime's per-request agent context independently of transport; on the CAPI transport the runtime derives the upstream `X-Interaction-Type` header from this same context. + [JsonPropertyName("interactionType")] + public string? InteractionType { get; set; } + + /// HTTP method, e.g. GET, POST. + [JsonPropertyName("method")] + public string Method { get; set; } = string.Empty; + + /// Stable identity of the immediate parent trajectory. Present for child trajectories such as subagents and conversation-sampling requests; absent for root-agent and non-agent requests. + [JsonPropertyName("parentAgentId")] + public string? ParentAgentId { get; set; } + + /// Opaque runtime-minted id, unique per in-flight request. The SDK uses this to correlate httpRequestChunk frames and to address its httpResponseStart / httpResponseChunk replies back to the runtime. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Id of the runtime session that triggered this request, when one is in scope. Absent for requests issued outside any session (e.g. startup model-catalog or capability resolution). This is a payload field — not a dispatch key — because the client-global API is registered process-wide rather than per session. + [JsonPropertyName("sessionId")] + public string? SessionId { get; set; } + + /// Transport the runtime would otherwise use for this request. `http` (the default when absent) covers plain HTTP and SSE responses; `websocket` indicates a full-duplex message channel where each body chunk maps to one WebSocket message and the `binary` flag distinguishes text from binary frames. The SDK consumer uses this to decide whether to service the request with an HTTP client or a WebSocket client. It is the one piece of request metadata the consumer cannot reliably infer from the URL or headers alone. + [JsonPropertyName("transport")] + public LlmInferenceHttpRequestStartTransport? Transport { get; set; } + + /// Absolute request URL. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; +} + +/// Acknowledgement. The SDK is free to ignore the ack and treat chunk delivery as fire-and-forget. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class LlmInferenceHttpRequestChunkResult +{ +} + +/// A request body chunk or cancellation signal. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class LlmInferenceHttpRequestChunkRequest +{ + /// Identity of the agent invocation (one agentic loop) this body chunk belongs to, matching the `agentInvocationId` semantics on httpRequestStart. Carried per chunk so a persistent transport can attribute successive turns correctly: when a WebSocket connection is reused across turns, the httpRequestStart identity reflects only the turn that opened the connection, so each later turn stamps its own invocation id here. Absent when the runtime has no invocation context for the request, or on the plain-HTTP transport where every request has its own httpRequestStart. + [JsonPropertyName("agentInvocationId")] + public string? AgentInvocationId { get; set; } + + /// When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text. + [JsonPropertyName("binary")] + public bool? Binary { get; set; } + + /// When true, the runtime is cancelling the in-flight request (e.g. upstream consumer aborted). `data` is ignored. Implies end-of-request. + [JsonPropertyName("cancel")] + public bool? Cancel { get; set; } + + /// Optional human-readable reason for the cancellation, propagated for logging. + [JsonPropertyName("cancelReason")] + public string? CancelReason { get; set; } + + /// Body byte range. UTF-8 text when `binary` is absent or false; base64-encoded bytes when `binary` is true. May be empty. + [JsonPropertyName("data")] + public string Data { get; set; } = string.Empty; + + /// When true, this is the final body chunk for the request. The SDK may rely on having received an end-marked chunk before treating the request body as complete. + [JsonPropertyName("end")] + public bool? End { get; set; } + + /// Matches the requestId from the originating httpRequestStart frame. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; +} + +/// Client environment metadata describing the process that produced a telemetry event. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class GitHubTelemetryClientInfo +{ + /// Copilot CLI version string. + [JsonPropertyName("cli_version")] + public string CliVersion { get; set; } = string.Empty; + + /// Name of the client application. + [JsonPropertyName("client_name")] + public string? ClientName { get; set; } + + /// Type of client. + [JsonPropertyName("client_type")] + public string? ClientType { get; set; } + + /// Copilot subscription plan, when known. + [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; } + + /// Whether the user is a GitHub/Microsoft staff member. + [JsonPropertyName("is_staff")] + public bool? IsStaff { get; set; } + + /// Node.js runtime version string. + [JsonPropertyName("node_version")] + public string NodeVersion { get; set; } = string.Empty; + + /// Operating system architecture (e.g. arm64, x64). + [JsonPropertyName("os_arch")] + public string OsArch { get; set; } = string.Empty; + + /// Operating system platform (e.g. darwin, linux, win32). + [JsonPropertyName("os_platform")] + public string OsPlatform { get; set; } = string.Empty; + + /// Operating system version string. + [JsonPropertyName("os_version")] + public string OsVersion { get; set; } = string.Empty; +} + +/// A single telemetry event in the runtime's native GitHub-shaped telemetry format, forwarded verbatim to opted-in hosts. The `restricted` flag on the enclosing GitHubTelemetryNotification distinguishes standard from restricted events; the payload shape is identical for both. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class GitHubTelemetryEvent +{ + /// Client environment metadata. + [JsonPropertyName("client")] + public GitHubTelemetryClientInfo? Client { get; set; } + + /// Copilot tracking ID for user-level attribution. + [JsonPropertyName("copilot_tracking_id")] + public string? CopilotTrackingId { get; set; } + + /// Timestamp when the event was created (ISO 8601 format). + [JsonPropertyName("created_at")] + public string? CreatedAt { get; set; } + + /// Experiment assignment context. + [JsonPropertyName("exp_assignment_context")] + public string? ExpAssignmentContext { get; set; } + + /// Feature flags enabled for this session, as a map from flag to value. + [JsonPropertyName("features")] + public IDictionary? Features { get; set; } + + /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; + + /// Numeric metrics as a map from key to value. + [JsonPropertyName("metrics")] + public IDictionary Metrics { get => field ??= new Dictionary(); set; } + + /// Reference to the model call that produced this event. + [JsonPropertyName("model_call_id")] + public string? ModelCallId { get; set; } + + /// String-valued properties as a map from key to value. + [JsonPropertyName("properties")] + public IDictionary Properties { get => field ??= new Dictionary(); set; } + + /// Session identifier the event belongs to. + [JsonPropertyName("session_id")] + public string? SessionId { get; set; } +} + +/// Payload for a `gitHubTelemetry.event` notification: a single GitHub telemetry event the runtime forwards to a host connection that opted into telemetry forwarding during the `server.connect` handshake. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class GitHubTelemetryNotification +{ + /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. + [JsonPropertyName("event")] + public GitHubTelemetryEvent Event { get => field ??= new(); set; } + + /// Whether this is a restricted telemetry event (cli.restricted_telemetry). Hosts must route restricted events to first-party Microsoft stores only. + [JsonPropertyName("restricted")] + public bool Restricted { get; set; } + + /// Session the telemetry event belongs to, when it is session-scoped. Omitted for sessionless events (for example, `server.sendTelemetry` calls with no session id), which are still forwarded to opted-in connections. + [JsonPropertyName("sessionId")] + public string? SessionId { get; set; } +} + +/// SDK host response to a GitHub credential request. +/// Polymorphic base type discriminated by kind. +[Experimental(global::GitHub.Copilot.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(global::GitHub.Copilot.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(global::GitHub.Copilot.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(global::GitHub.Copilot.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; } +} + +/// A response is meaningful only on the connection and request that issued its challenge. +public sealed class InstallationsConfirmResult +{ + /// Exact challenge from the request. + [JsonPropertyName("confirmationId")] + public string ConfirmationId { get; set; } = string.Empty; + + /// Fresh explicit user decision. There is no default. + [JsonPropertyName("decision")] + public InstallationDecision Decision { get; set; } + + /// Exact review commitment from the request. + [JsonPropertyName("reviewFingerprint")] + public string ReviewFingerprint { get; set; } = string.Empty; +} + +/// Only resource kinds with an implemented installation engine have a review variant. +/// Polymorphic base type discriminated by resource. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "resource", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(InstallationReviewMcp), "mcp")] +[JsonDerivedType(typeof(InstallationReviewSkill), "skill")] +public partial class InstallationReview +{ + /// The type discriminator. + [JsonPropertyName("resource")] + public virtual string Resource { get; set; } = string.Empty; +} + + +/// Safe MCP review fields. No raw card, retrieval URL, plan handle or secret value. +/// Polymorphic base type discriminated by action. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "action", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpInstallationReviewInstall), "install")] +[JsonDerivedType(typeof(McpInstallationReviewUninstall), "uninstall")] +public partial class McpInstallationReview +{ + /// The type discriminator. + [JsonPropertyName("action")] + public virtual string Action { get; set; } = string.Empty; +} + + +/// +/// Final remote configuration, not a template. The producer refuses external-value +/// expansion before presenting this review. Receipt-owned secrets appear only as +/// `${installation-secret:<id>}` references whose `<id>` matches a reviewed +/// `${secret:<id>}` placeholder; values are never included. +/// +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpInstallationRemoteConfiguration +{ + /// + /// Configured headers, excluding separately authorised OAuth tokens. Values may + /// contain owned secret references, never secret values. + /// + [JsonPropertyName("headers")] + public IDictionary Headers { get => field ??= new Dictionary(); set; } + + /// Configured tool selection, not permission to invoke those tools. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } + + /// Transport in the effective persisted remote configuration. + [JsonPropertyName("transport")] + public McpPlanRemoteTransport Transport { get; set; } + + /// Exact resolved endpoint, without templates or secret placeholders. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; +} + +/// The install variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpInstallationReviewInstall : McpInstallationReview +{ + /// + [JsonIgnore] + public override string Action => "install"; + + /// Catalogue identity retained from the bound candidate when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("catalogue")] + public InstallationCatalogueIdentity? Catalogue { get; set; } /// Original catalogue trust metadata, not a verification claim. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("catalogueTrust")] public CatalogTrustSnapshot? CatalogueTrust { get; set; } - /// The configuration change for the selected alternative only. - [JsonPropertyName("configurationChange")] - public required McpPlanConfigurationChange ConfigurationChange { get; set; } + /// The configuration change for the selected alternative only. + [JsonPropertyName("configurationChange")] + public required McpPlanConfigurationChange ConfigurationChange { get; set; } + + /// + /// Complete effective remote configuration for final installation review. + /// Earlier private selection reviews and package choices omit this field. + /// The owned remote resource requires it before issuing confirmation. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("effectiveConfiguration")] + public McpInstallationRemoteConfiguration? EffectiveConfiguration { get; set; } + + /// Identity from the retained plan, not caller display text. + [JsonPropertyName("identity")] + public required McpPlanResourceIdentity Identity { get; set; } + + /// Non-secret values supplied for this selected alternative. + [JsonPropertyName("inputs")] + public required IList Inputs { get; set; } + + /// Policy decision bound to this plan. + [JsonPropertyName("policy")] + public required McpPlanPolicyResult Policy { get; set; } + + /// Original source identity and content commitment. + [JsonPropertyName("provenance")] + public required McpPlanProvenance Provenance { get; set; } + + /// Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. + [JsonPropertyName("secretStorage")] + public required McpInstallationSecretStorage SecretStorage { get; set; } + + /// Only the selected alternative is applied. + [JsonPropertyName("selectedChoice")] + public required McpPlanTransportChoice SelectedChoice { get; set; } + + /// Exact reviewed placeholders supplied separately. Never secret values. + [JsonPropertyName("suppliedSecrets")] + public required IList SuppliedSecrets { get; set; } + + /// Exact reviewed user-scope destination. + [JsonPropertyName("target")] + public required McpPlanTarget Target { get; set; } +} + +/// The uninstall variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpInstallationReviewUninstall : McpInstallationReview +{ + /// + [JsonIgnore] + public override string Action => "uninstall"; + + /// Identity from the installed receipt. + [JsonPropertyName("identity")] + public required McpPlanResourceIdentity Identity { get; set; } + + /// Receipt-owned installation being removed. + [JsonPropertyName("installationId")] + public required string InstallationId { get; set; } + + /// Exact planner-owned secret slots to remove, excluding shared OAuth grants. + [JsonPropertyName("ownedSecretCount")] + public required long OwnedSecretCount { get; set; } + + /// Current removal policy, independent of permission to activate the server. + [JsonPropertyName("policy")] + public required McpPlanPolicyResult Policy { get; set; } + + /// Shared profile authentication is deliberately retained, not pending cleanup. + [JsonPropertyName("preservesSharedAuthentication")] + public required bool PreservesSharedAuthentication { get; set; } + + /// Source identity and content commitment retained by the installed receipt. + [JsonPropertyName("provenance")] + public required McpPlanProvenance Provenance { get; set; } + + /// Whether uninstall restores a protected pre-install configuration. + [JsonPropertyName("restoresPreviousConfiguration")] + public required bool RestoresPreviousConfiguration { get; set; } + + /// Exact destination, checked for intervening changes before mutation. + [JsonPropertyName("target")] + public required McpPlanTarget Target { get; set; } +} + +/// The mcp variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class InstallationReviewMcp : InstallationReview +{ + /// + [JsonIgnore] + public override string Resource => "mcp"; + + /// The exact MCP action and its reviewed changes. + [JsonPropertyName("review")] + public required McpInstallationReview Review { get; set; } +} + +/// The skill variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class InstallationReviewSkill : InstallationReview +{ + /// + [JsonIgnore] + public override string Resource => "skill"; + + /// The exact verified Skill action and its reviewed files. + [JsonPropertyName("review")] + public required SkillInstallationReview Review { get; set; } +} + +/// One connection-owned, expiring request for a trusted host's explicit user decision. +public sealed class InstallationsConfirmRequest +{ + /// Opaque one-use challenge. Return unchanged; never log or persist. + [JsonPropertyName("confirmationId")] + public string ConfirmationId { get; set; } = string.Empty; + + /// Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. + [JsonPropertyName("expiresAt")] + public string ExpiresAt { get; set; } = string.Empty; + + /// Random identifier of this installation operation, not a plan handle. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; + + /// + /// Original engine-resolved selector for a bound operation, never a dispatch default. + /// Bound MCP confirmation always includes it; correlate it with the original pending action. + /// + [JsonPropertyName("policySessionId")] + public string? PolicySessionId { get; set; } + + /// Resource-specific review to present before collecting the user's decision. + [JsonPropertyName("review")] + public InstallationReview Review { get => field ??= new(); set; } + + /// Opaque commitment to the exact review and inputs. Return unchanged; never log. + [JsonPropertyName("reviewFingerprint")] + public string ReviewFingerprint { get; set; } = string.Empty; +} + +/// Closed set of public task kinds a connection can negotiate. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runtime-owned background agent task. + public static TaskKind Agent { get; } = new("agent"); + + /// Runtime-owned shell task. + public static TaskKind Shell { get; } = new("shell"); + + /// Client-owned externally executed task. + public static TaskKind Client { get; } = new("client"); - /// - /// Complete effective remote configuration for final installation review. - /// Earlier private selection reviews and package choices omit this field. - /// The owned remote resource requires it before issuing confirmation. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("effectiveConfiguration")] - public McpInstallationRemoteConfiguration? EffectiveConfiguration { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskKind left, TaskKind right) => left.Equals(right); - /// Identity from the retained plan, not caller display text. - [JsonPropertyName("identity")] - public required McpPlanResourceIdentity Identity { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskKind left, TaskKind right) => !(left == right); - /// Non-secret values supplied for this selected alternative. - [JsonPropertyName("inputs")] - public required IList Inputs { get; set; } + /// + public override bool Equals(object? obj) => obj is TaskKind other && Equals(other); - /// Policy decision bound to this plan. - [JsonPropertyName("policy")] - public required McpPlanPolicyResult Policy { get; set; } + /// + public bool Equals(TaskKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Original source identity and content commitment. - [JsonPropertyName("provenance")] - public required McpPlanProvenance Provenance { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. - [JsonPropertyName("secretStorage")] - public required McpInstallationSecretStorage SecretStorage { get; set; } + /// + public override string ToString() => Value; - /// Only the selected alternative is applied. - [JsonPropertyName("selectedChoice")] - public required McpPlanTransportChoice SelectedChoice { get; set; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Exact reviewed placeholders supplied separately. Never secret values. - [JsonPropertyName("suppliedSecrets")] - public required IList SuppliedSecrets { get; set; } + /// + public override void Write(Utf8JsonWriter writer, TaskKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskKind)); + } + } +} - /// Exact reviewed user-scope destination. - [JsonPropertyName("target")] - public required McpPlanTarget Target { get; set; } + +/// GitHub Mission Control compute kind. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EnvironmentKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EnvironmentKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A user-managed environment on a local machine. + public static EnvironmentKind UserLocal { get; } = new("user-local"); + + /// A user-managed environment in a GitHub Codespace. + public static EnvironmentKind UserCodespace { get; } = new("user-codespace"); + + /// A GitHub-managed environment backed by GitHub Actions. + public static EnvironmentKind ManagedActions { get; } = new("managed-actions"); + + /// A GitHub-managed sandbox environment. + public static EnvironmentKind ManagedSandbox { get; } = new("managed-sandbox"); + + /// A GitHub-managed cloud coding agent environment. + public static EnvironmentKind ManagedCca { get; } = new("managed-cca"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EnvironmentKind left, EnvironmentKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EnvironmentKind left, EnvironmentKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EnvironmentKind other && Equals(other); + + /// + public bool Equals(EnvironmentKind 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 EnvironmentKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EnvironmentKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EnvironmentKind)); + } + } +} + + +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runs before a tool is invoked. + public static HookType PreToolUse { get; } = new("preToolUse"); + + /// Runs before an MCP tool is invoked. + public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); + + /// Runs after a tool completes successfully. + public static HookType PostToolUse { get; } = new("postToolUse"); + + /// Runs after a tool fails. + public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + + /// Runs after the user submits a prompt. + public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + public static HookType UserPromptTransformed { get; } = new("userPromptTransformed"); + + /// Runs when a session starts. + public static HookType SessionStart { get; } = new("sessionStart"); + + /// Runs when a session ends. + public static HookType SessionEnd { get; } = new("sessionEnd"); + + /// Runs after an agent result is produced. + public static HookType PostResult { get; } = new("postResult"); + + /// Runs before a pull request description is generated. + public static HookType PrePRDescription { get; } = new("prePRDescription"); + + /// Runs when the agent encounters an error. + public static HookType ErrorOccurred { get; } = new("errorOccurred"); + + /// Runs when the agent stops. + public static HookType AgentStop { get; } = new("agentStop"); + + /// Runs when a subagent starts. + public static HookType SubagentStart { get; } = new("subagentStart"); + + /// Runs when a subagent stops. + public static HookType SubagentStop { get; } = new("subagentStop"); + + /// Runs before conversation context is compacted. + public static HookType PreCompact { get; } = new("preCompact"); + + /// Runs when the agent requests permission. + public static HookType PermissionRequest { get; } = new("permissionRequest"); + + /// Runs when the agent emits a notification. + public static HookType Notification { get; } = new("notification"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookType left, HookType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookType left, HookType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookType other && Equals(other); + + /// + public bool Equals(HookType 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 HookType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + } + } +} + + +/// Configuration tier that contributed a discovered hook action. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookOrigin : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookOrigin(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Hook loaded from user settings or the user's hook directory. + public static HookOrigin User { get; } = new("user"); + + /// Hook loaded from repository settings or the repository hook directory. + public static HookOrigin Repository { get; } = new("repository"); + + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + public static HookOrigin Plugin { get; } = new("plugin"); + + /// Hook enforced by centrally managed policy. + public static HookOrigin Policy { get; } = new("policy"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookOrigin left, HookOrigin right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookOrigin left, HookOrigin right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookOrigin other && Equals(other); + + /// + public bool Equals(HookOrigin 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 HookOrigin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookOrigin value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + } + } +} + + +/// Resolved Anthropic adaptive-thinking capability for a model. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AdaptiveThinkingSupport : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AdaptiveThinkingSupport(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The model does not accept thinking.type='adaptive'. + public static AdaptiveThinkingSupport Unsupported { get; } = new("unsupported"); + + /// The model accepts adaptive thinking but also accepts thinking.type='enabled'. + public static AdaptiveThinkingSupport Optional { get; } = new("optional"); + + /// The model defaults to adaptive thinking and rejects thinking.type='enabled' with HTTP 400, but still accepts thinking.type='disabled' (e.g. opus-4.7/4.8/5, sonnet-5). + public static AdaptiveThinkingSupport Required { get; } = new("required"); + + /// The model accepts only thinking.type='adaptive'; 'enabled', 'disabled', and an omitted thinking block all fail with HTTP 400 (e.g. fable, mythos). + public static AdaptiveThinkingSupport AdaptiveOnly { get; } = new("adaptive_only"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AdaptiveThinkingSupport other && Equals(other); + + /// + public bool Equals(AdaptiveThinkingSupport 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 AdaptiveThinkingSupport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AdaptiveThinkingSupport value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AdaptiveThinkingSupport)); + } + } +} + + +/// Model capability category for grouping in the model picker. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPickerCategory : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPickerCategory(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Lightweight model category optimized for faster, lower-cost interactions. + public static ModelPickerCategory Lightweight { get; } = new("lightweight"); + + /// Versatile model category suitable for a broad range of tasks. + public static ModelPickerCategory Versatile { get; } = new("versatile"); + + /// Powerful model category optimized for complex tasks. + public static ModelPickerCategory Powerful { get; } = new("powerful"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPickerCategory left, ModelPickerCategory right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPickerCategory left, ModelPickerCategory right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelPickerCategory other && Equals(other); + + /// + public bool Equals(ModelPickerCategory 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 ModelPickerCategory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelPickerCategory value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerCategory)); + } + } +} + + +/// Relative cost tier for token-based billing users. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPickerPriceCategory : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPickerPriceCategory(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Lowest relative token cost tier. + public static ModelPickerPriceCategory Low { get; } = new("low"); + + /// Medium relative token cost tier. + public static ModelPickerPriceCategory Medium { get; } = new("medium"); + + /// High relative token cost tier. + public static ModelPickerPriceCategory High { get; } = new("high"); + + /// Highest relative token cost tier. + public static ModelPickerPriceCategory VeryHigh { get; } = new("very_high"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelPickerPriceCategory other && Equals(other); + + /// + public bool Equals(ModelPickerPriceCategory 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 ModelPickerPriceCategory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelPickerPriceCategory value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerPriceCategory)); + } + } } -/// The uninstall variant of . + +/// Current policy state for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpInstallationReviewUninstall : McpInstallationReview +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPolicyState : IEquatable { - /// - [JsonIgnore] - public override string Action => "uninstall"; + private readonly string? _value; - /// Identity from the installed receipt. - [JsonPropertyName("identity")] - public required McpPlanResourceIdentity Identity { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPolicyState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Receipt-owned installation being removed. - [JsonPropertyName("installationId")] - public required string InstallationId { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Exact planner-owned secret slots to remove, excluding shared OAuth grants. - [JsonPropertyName("ownedSecretCount")] - public required long OwnedSecretCount { get; set; } + /// The model is enabled by policy. + public static ModelPolicyState Enabled { get; } = new("enabled"); - /// Current removal policy, independent of permission to activate the server. - [JsonPropertyName("policy")] - public required McpPlanPolicyResult Policy { get; set; } + /// The model is disabled by policy. + public static ModelPolicyState Disabled { get; } = new("disabled"); - /// Shared profile authentication is deliberately retained, not pending cleanup. - [JsonPropertyName("preservesSharedAuthentication")] - public required bool PreservesSharedAuthentication { get; set; } + /// No explicit policy is configured for the model. + public static ModelPolicyState Unconfigured { get; } = new("unconfigured"); - /// Source identity and content commitment retained by the installed receipt. - [JsonPropertyName("provenance")] - public required McpPlanProvenance Provenance { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPolicyState left, ModelPolicyState right) => left.Equals(right); - /// Whether uninstall restores a protected pre-install configuration. - [JsonPropertyName("restoresPreviousConfiguration")] - public required bool RestoresPreviousConfiguration { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPolicyState left, ModelPolicyState right) => !(left == right); - /// Exact destination, checked for intervening changes before mutation. - [JsonPropertyName("target")] - public required McpPlanTarget Target { get; set; } -} + /// + public override bool Equals(object? obj) => obj is ModelPolicyState other && Equals(other); -/// The mcp variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class InstallationReviewMcp : InstallationReview -{ /// - [JsonIgnore] - public override string Resource => "mcp"; + public bool Equals(ModelPolicyState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// The exact MCP action and its reviewed changes. - [JsonPropertyName("review")] - public required McpInstallationReview Review { get; set; } -} + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); -/// The skill variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class InstallationReviewSkill : InstallationReview -{ /// - [JsonIgnore] - public override string Resource => "skill"; + public override string ToString() => Value; - /// The exact verified Skill action and its reviewed files. - [JsonPropertyName("review")] - public required SkillInstallationReview Review { get; set; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ModelPolicyState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelPolicyState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPolicyState)); + } + } } -/// One connection-owned, expiring request for a trusted host's explicit user decision. -public sealed class InstallationsConfirmRequest + +/// The neutral kind of a model provider — the model analog of `AccountKind`. A model provider is the live, entitled source a model came from; central code never branches on this beyond a single dispatch. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelProviderKind : IEquatable { - /// Opaque one-use challenge. Return unchanged; never log or persist. - [JsonPropertyName("confirmationId")] - public string ConfirmationId { get; set; } = string.Empty; + private readonly string? _value; - /// Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. - [JsonPropertyName("expiresAt")] - public string ExpiresAt { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelProviderKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Random identifier of this installation operation, not a plan handle. - [JsonPropertyName("operationId")] - public string OperationId { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// - /// Original engine-resolved selector for a bound operation, never a dispatch default. - /// Bound MCP confirmation always includes it; correlate it with the original pending action. - /// - [JsonPropertyName("policySessionId")] - public string? PolicySessionId { get; set; } + /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. + public static ModelProviderKind Copilot { get; } = new("copilot"); - /// Resource-specific review to present before collecting the user's decision. - [JsonPropertyName("review")] - public InstallationReview Review { get => field ??= new(); set; } + /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. + public static ModelProviderKind Loki { get; } = new("loki"); - /// Opaque commitment to the exact review and inputs. Return unchanged; never log. - [JsonPropertyName("reviewFingerprint")] - public string ReviewFingerprint { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderKind left, ModelProviderKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderKind left, ModelProviderKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelProviderKind other && Equals(other); + + /// + public bool Equals(ModelProviderKind 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 ModelProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelProviderKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderKind)); + } + } } -/// Closed set of public task kinds a connection can negotiate. + +/// State of the persistent certificate authority of the sandbox credential proxy. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskKind : IEquatable +public readonly struct SandboxProxyCaState : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskKind(string value) + public SandboxProxyCaState(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Runtime-owned background agent task. - public static TaskKind Agent { get; } = new("agent"); + /// This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. + public static SandboxProxyCaState Unsupported { get; } = new("unsupported"); - /// Runtime-owned shell task. - public static TaskKind Shell { get; } = new("shell"); + /// OS trust does not include the certificate authority, or none is stored. + public static SandboxProxyCaState NotInstalled { get; } = new("notInstalled"); - /// Client-owned externally executed task. - public static TaskKind Client { get; } = new("client"); + /// OS trust includes the stored certificate authority. + public static SandboxProxyCaState Installed { get; } = new("installed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskKind left, TaskKind right) => left.Equals(right); + /// The runtime could not read the certificate authority or the OS trust store. + public static SandboxProxyCaState Error { get; } = new("error"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskKind left, TaskKind right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SandboxProxyCaState left, SandboxProxyCaState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SandboxProxyCaState left, SandboxProxyCaState right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskKind other && Equals(other); + public override bool Equals(object? obj) => obj is SandboxProxyCaState other && Equals(other); /// - public bool Equals(TaskKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SandboxProxyCaState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -25903,71 +28194,68 @@ public TaskKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SandboxProxyCaState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SandboxProxyCaState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SandboxProxyCaState)); } } } -/// GitHub Mission Control compute kind. +/// Server transport type: stdio, http, sse (deprecated), or memory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct EnvironmentKind : IEquatable +public readonly struct DiscoveredMcpServerType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public EnvironmentKind(string value) + public DiscoveredMcpServerType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A user-managed environment on a local machine. - public static EnvironmentKind UserLocal { get; } = new("user-local"); - - /// A user-managed environment in a GitHub Codespace. - public static EnvironmentKind UserCodespace { get; } = new("user-codespace"); + /// Server communicates over stdio with a local child process. + public static DiscoveredMcpServerType Stdio { get; } = new("stdio"); - /// A GitHub-managed environment backed by GitHub Actions. - public static EnvironmentKind ManagedActions { get; } = new("managed-actions"); + /// Server communicates over streamable HTTP. + public static DiscoveredMcpServerType Http { get; } = new("http"); - /// A GitHub-managed sandbox environment. - public static EnvironmentKind ManagedSandbox { get; } = new("managed-sandbox"); + /// Server communicates over Server-Sent Events (deprecated). + public static DiscoveredMcpServerType Sse { get; } = new("sse"); - /// A GitHub-managed cloud coding agent environment. - public static EnvironmentKind ManagedCca { get; } = new("managed-cca"); + /// Server is backed by an in-memory runtime implementation. + public static DiscoveredMcpServerType Memory { get; } = new("memory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EnvironmentKind left, EnvironmentKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EnvironmentKind left, EnvironmentKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is EnvironmentKind other && Equals(other); + public override bool Equals(object? obj) => obj is DiscoveredMcpServerType other && Equals(other); /// - public bool Equals(EnvironmentKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiscoveredMcpServerType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -25975,107 +28263,98 @@ public EnvironmentKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override EnvironmentKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiscoveredMcpServerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, EnvironmentKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiscoveredMcpServerType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EnvironmentKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredMcpServerType)); } } } -/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +/// A wire feature a caller can require of the catalog surface, negotiated per request. A grant means the runtime understands the feature's contract, not that the deployment has enabled the operation; typed unavailable results report availability separately. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HookType : IEquatable +public readonly struct CatalogCapability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public HookType(string value) + public CatalogCapability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Runs before a tool is invoked. - public static HookType PreToolUse { get; } = new("preToolUse"); - - /// Runs before an MCP tool is invoked. - public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); - - /// Runs after a tool completes successfully. - public static HookType PostToolUse { get; } = new("postToolUse"); - - /// Runs after a tool fails. - public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + /// Understands the current `application/mcp-server-card+json` media type. + public static CatalogCapability McpServerCard { get; } = new("mcp-server-card"); - /// Runs after the user submits a prompt. - public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + /// Understands the legacy `application/mcp-server+json` media type. + public static CatalogCapability LegacyMcpServerCard { get; } = new("legacy-mcp-server-card"); - /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. - public static HookType UserPromptTransformed { get; } = new("userPromptTransformed"); + /// Understands `application/ai-skill` candidates as discovery-only and typed non-installable. + public static CatalogCapability AiSkillDiscovery { get; } = new("ai-skill-discovery"); - /// Runs when a session starts. - public static HookType SessionStart { get; } = new("sessionStart"); + /// Understands opt-in `application/vnd.github.copilot-plugin` candidates and their typed identity, version, source, and compatibility fields. + public static CatalogCapability AgentPluginDiscovery { get; } = new("agent-plugin-discovery"); - /// Runs when a session ends. - public static HookType SessionEnd { get; } = new("sessionEnd"); + /// Understands side-effect-free MCP install-plan requests, results, and plan handles; `planning-unavailable` separately reports that planning is not enabled. + public static CatalogCapability McpInstallPlanning { get; } = new("mcp-install-planning"); - /// Runs after an agent result is produced. - public static HookType PostResult { get; } = new("postResult"); + /// Understands plans that enumerate every eligible transport rather than a single preferred one. + public static CatalogCapability MultipleTransportChoice { get; } = new("multiple-transport-choice"); - /// Runs before a pull request description is generated. - public static HookType PrePRDescription { get; } = new("prePRDescription"); + /// Understands explicit numbered navigation and authority-reported pagination metadata with opaque tokens. Advertised and granted only when requested. + public static CatalogCapability CatalogSearchPagination { get; } = new("catalog-search-pagination"); - /// Runs when the agent encounters an error. - public static HookType ErrorOccurred { get; } = new("errorOccurred"); + /// Understands versioned candidate trust snapshots. Protocol-3 callers must require this capability before the runtime adds the optional snapshot field. + public static CatalogCapability TrustSnapshot { get; } = new("trust-snapshot"); - /// Runs when the agent stops. - public static HookType AgentStop { get; } = new("agentStop"); + /// Understands exact candidate selection through model-safe opaque references and host-only candidate-handle hand-off. + public static CatalogCapability CatalogSelection { get; } = new("catalog-selection"); - /// Runs when a subagent starts. - public static HookType SubagentStart { get; } = new("subagentStart"); + /// Requires an eligible credential for the selected GitHub.com account before search egress and prohibits client-side anonymous retry, including after HTTP 401 or 403. The credential is scoped to the fixed catalog authority without redirect forwarding. Neither a grant nor successful response proves that the authority accepted the identity or selected a particular backend. Preserve this requirement on every page and retry; callers omitting it retain optional authentication. + public static CatalogCapability CatalogSearchCredentialRequired { get; } = new("catalog-search-credential-required"); - /// Runs when a subagent stops. - public static HookType SubagentStop { get; } = new("subagentStop"); + /// Captures the exact existing native session, account, host and connection for authenticated catalogue search, selection and planning. Requires catalog-search-credential-required; does not grant installation or create a session. + public static CatalogCapability CatalogSearchSessionBound { get; } = new("catalog-search-session-bound"); - /// Runs before conversation context is compacted. - public static HookType PreCompact { get; } = new("preCompact"); + /// Understands effect-free preparation, exact human-confirmed apply and owned removal for fully resolved personal remote MCP choices without supplied inputs or configured secrets. Advertised only when the real producer and lower owned admission are linked; requires original connection and bound session authority for new work. + public static CatalogCapability McpConfirmedRemoteInstallation { get; } = new("mcp-confirmed-remote-installation"); - /// Runs when the agent requests permission. - public static HookType PermissionRequest { get; } = new("permissionRequest"); + /// Extends mcp-confirmed-remote-installation to declared non-secret header and URL values and receipt-owned header secrets for personal remote MCP choices. Requires mcp-confirmed-remote-installation and bound session authority. Secret values are written only to the reviewed backend after confirmation and are never returned; package and stdio choices remain unsupported. Advertised only when owned secret effects and owned secret activation are linked. + public static CatalogCapability McpConfiguredRemoteInstallation { get; } = new("mcp-configured-remote-installation"); - /// Runs when the agent emits a notification. - public static HookType Notification { get; } = new("notification"); + /// Understands verified Agent Finder Skill install, uninstall, recovery and installation-scoped enablement APIs. Advertised only by runtimes with the Skill installation engine linked; acquisition may still be refused as feature-disabled per selected session. + public static CatalogCapability SkillConfirmedInstallation { get; } = new("skill-confirmed-installation"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(HookType left, HookType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogCapability left, CatalogCapability right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(HookType left, HookType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogCapability left, CatalogCapability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is HookType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogCapability other && Equals(other); /// - public bool Equals(HookType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26083,68 +28362,122 @@ public HookType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override HookType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, HookType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogCapability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCapability)); } } } -/// Configuration tier that contributed a discovered hook action. +/// Whether a planned configuration change would create or modify an entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HookOrigin : IEquatable +public readonly struct McpPlanConfigurationOperation : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public HookOrigin(string value) + public McpPlanConfigurationOperation(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Hook loaded from user settings or the user's hook directory. - public static HookOrigin User { get; } = new("user"); + /// Creates a configuration entry that does not exist yet. + public static McpPlanConfigurationOperation Add { get; } = new("add"); - /// Hook loaded from repository settings or the repository hook directory. - public static HookOrigin Repository { get; } = new("repository"); + /// Modifies a configuration entry that already exists. + public static McpPlanConfigurationOperation Update { get; } = new("update"); - /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. - public static HookOrigin Plugin { get; } = new("plugin"); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => left.Equals(right); - /// Hook enforced by centrally managed policy. - public static HookOrigin Policy { get; } = new("policy"); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => !(left == right); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(HookOrigin left, HookOrigin right) => left.Equals(right); + /// + public override bool Equals(object? obj) => obj is McpPlanConfigurationOperation other && Equals(other); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(HookOrigin left, HookOrigin right) => !(left == right); + /// + public bool Equals(McpPlanConfigurationOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// - public override bool Equals(object? obj) => obj is HookOrigin other && Equals(other); + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); /// - public bool Equals(HookOrigin other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpPlanConfigurationOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpPlanConfigurationOperation value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanConfigurationOperation)); + } + } +} + + +/// Configuration scope an MCP install plan targets. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanScope : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanScope(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The user's own MCP configuration. + public static McpPlanScope User { get; } = new("user"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanScope left, McpPlanScope right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanScope left, McpPlanScope right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpPlanScope other && Equals(other); + + /// + public bool Equals(McpPlanScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26152,68 +28485,65 @@ public HookOrigin(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override HookOrigin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, HookOrigin value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScope)); } } } -/// Resolved Anthropic adaptive-thinking capability for a model. +/// What policy decided for a planned server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AdaptiveThinkingSupport : IEquatable +public readonly struct McpPlanPolicyDecision : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AdaptiveThinkingSupport(string value) + public McpPlanPolicyDecision(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The model does not accept thinking.type='adaptive'. - public static AdaptiveThinkingSupport Unsupported { get; } = new("unsupported"); - - /// The model accepts adaptive thinking but also accepts thinking.type='enabled'. - public static AdaptiveThinkingSupport Optional { get; } = new("optional"); + /// Policy permits the server. + public static McpPlanPolicyDecision Allowed { get; } = new("allowed"); - /// The model defaults to adaptive thinking and rejects thinking.type='enabled' with HTTP 400, but still accepts thinking.type='disabled' (e.g. opus-4.7/4.8/5, sonnet-5). - public static AdaptiveThinkingSupport Required { get; } = new("required"); + /// Policy forbids the server, so the plan cannot be applied. + public static McpPlanPolicyDecision Blocked { get; } = new("blocked"); - /// The model accepts only thinking.type='adaptive'; 'enabled', 'disabled', and an omitted thinking block all fail with HTTP 400 (e.g. fable, mythos). - public static AdaptiveThinkingSupport AdaptiveOnly { get; } = new("adaptive_only"); + /// Policy permits the server only after an explicit approval. + public static McpPlanPolicyDecision RequiresApproval { get; } = new("requires-approval"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AdaptiveThinkingSupport other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanPolicyDecision other && Equals(other); /// - public bool Equals(AdaptiveThinkingSupport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPlanPolicyDecision other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26221,65 +28551,68 @@ public AdaptiveThinkingSupport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AdaptiveThinkingSupport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanPolicyDecision Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AdaptiveThinkingSupport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanPolicyDecision value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AdaptiveThinkingSupport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicyDecision)); } } } -/// Model capability category for grouping in the model picker. +/// Which authority produced a policy decision. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPickerCategory : IEquatable +public readonly struct McpPlanPolicySource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ModelPickerCategory(string value) + public McpPlanPolicySource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Lightweight model category optimized for faster, lower-cost interactions. - public static ModelPickerCategory Lightweight { get; } = new("lightweight"); + /// No policy applied, so the server is permitted by default. + public static McpPlanPolicySource None { get; } = new("none"); - /// Versatile model category suitable for a broad range of tasks. - public static ModelPickerCategory Versatile { get; } = new("versatile"); + /// An enterprise allowlist evaluated the server. + public static McpPlanPolicySource EnterpriseAllowlist { get; } = new("enterprise-allowlist"); - /// Powerful model category optimized for complex tasks. - public static ModelPickerCategory Powerful { get; } = new("powerful"); + /// The registry the card came from evaluated the server. + public static McpPlanPolicySource RegistryPolicy { get; } = new("registry-policy"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelPickerCategory left, ModelPickerCategory right) => left.Equals(right); + /// Local trust settings evaluated the server. + public static McpPlanPolicySource LocalTrust { get; } = new("local-trust"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelPickerCategory left, ModelPickerCategory right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanPolicySource left, McpPlanPolicySource right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanPolicySource left, McpPlanPolicySource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelPickerCategory other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanPolicySource other && Equals(other); /// - public bool Equals(ModelPickerCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPlanPolicySource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26287,68 +28620,59 @@ public ModelPickerCategory(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ModelPickerCategory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanPolicySource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ModelPickerCategory value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanPolicySource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicySource)); } } } -/// Relative cost tier for token-based billing users. +/// Canonical digest algorithm for a validated MCP card. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPickerPriceCategory : IEquatable +public readonly struct CardDigestAlgorithm : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ModelPickerPriceCategory(string value) + public CardDigestAlgorithm(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Lowest relative token cost tier. - public static ModelPickerPriceCategory Low { get; } = new("low"); - - /// Medium relative token cost tier. - public static ModelPickerPriceCategory Medium { get; } = new("medium"); - - /// High relative token cost tier. - public static ModelPickerPriceCategory High { get; } = new("high"); - - /// Highest relative token cost tier. - public static ModelPickerPriceCategory VeryHigh { get; } = new("very_high"); + /// SHA-256 over RFC 8785 canonical JSON encoded as UTF-8. + public static CardDigestAlgorithm Sha256Rfc8785 { get; } = new("sha256-rfc8785"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CardDigestAlgorithm left, CardDigestAlgorithm right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CardDigestAlgorithm left, CardDigestAlgorithm right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelPickerPriceCategory other && Equals(other); + public override bool Equals(object? obj) => obj is CardDigestAlgorithm other && Equals(other); /// - public bool Equals(ModelPickerPriceCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CardDigestAlgorithm other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26356,65 +28680,62 @@ public ModelPickerPriceCategory(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ModelPickerPriceCategory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CardDigestAlgorithm Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ModelPickerPriceCategory value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CardDigestAlgorithm value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerPriceCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CardDigestAlgorithm)); } } } -/// Current policy state for this model. +/// JSON MCP card media type accepted for install planning. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPolicyState : IEquatable +public readonly struct McpServerCardMediaType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ModelPolicyState(string value) + public McpServerCardMediaType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The model is enabled by policy. - public static ModelPolicyState Enabled { get; } = new("enabled"); - - /// The model is disabled by policy. - public static ModelPolicyState Disabled { get; } = new("disabled"); + /// The current MCP server card media type. + public static McpServerCardMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); - /// No explicit policy is configured for the model. - public static ModelPolicyState Unconfigured { get; } = new("unconfigured"); + /// The legacy MCP server card media type, accepted for compatibility. + public static McpServerCardMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelPolicyState left, ModelPolicyState right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpServerCardMediaType left, McpServerCardMediaType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelPolicyState left, ModelPolicyState right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpServerCardMediaType left, McpServerCardMediaType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelPolicyState other && Equals(other); + public override bool Equals(object? obj) => obj is McpServerCardMediaType other && Equals(other); /// - public bool Equals(ModelPolicyState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpServerCardMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26422,62 +28743,71 @@ public ModelPolicyState(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ModelPolicyState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpServerCardMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ModelPolicyState value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpServerCardMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPolicyState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpServerCardMediaType)); } } } -/// The neutral kind of a model provider — the model analog of `AccountKind`. A model provider is the live, entitled source a model came from; central code never branches on this beyond a single dispatch. +/// Where a required value is applied when the planned server is launched. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelProviderKind : IEquatable +public readonly struct McpPlanValueCategory : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ModelProviderKind(string value) + public McpPlanValueCategory(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. - public static ModelProviderKind Copilot { get; } = new("copilot"); + /// Set as an environment variable on the launched process. + public static McpPlanValueCategory EnvironmentVariable { get; } = new("environment-variable"); - /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. - public static ModelProviderKind Loki { get; } = new("loki"); + /// Passed to the runtime that launches the package. + public static McpPlanValueCategory RuntimeArgument { get; } = new("runtime-argument"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelProviderKind left, ModelProviderKind right) => left.Equals(right); + /// Passed to the packaged server itself. + public static McpPlanValueCategory PackageArgument { get; } = new("package-argument"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelProviderKind left, ModelProviderKind right) => !(left == right); + /// Sent as a request header to a remote endpoint. + public static McpPlanValueCategory Header { get; } = new("header"); + + /// Substituted into the remote endpoint URL. + public static McpPlanValueCategory UrlVariable { get; } = new("url-variable"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanValueCategory left, McpPlanValueCategory right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanValueCategory left, McpPlanValueCategory right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelProviderKind other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanValueCategory other && Equals(other); /// - public bool Equals(ModelProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPlanValueCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26485,68 +28815,68 @@ public ModelProviderKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ModelProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanValueCategory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ModelProviderKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanValueCategory value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanValueCategory)); } } } -/// Server transport type: stdio, http, sse (deprecated), or memory. +/// Scalar type a required value must conform to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiscoveredMcpServerType : IEquatable +public readonly struct McpPlanScalarValueType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiscoveredMcpServerType(string value) + public McpPlanScalarValueType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Server communicates over stdio with a local child process. - public static DiscoveredMcpServerType Stdio { get; } = new("stdio"); + /// Free text. + public static McpPlanScalarValueType String { get; } = new("string"); - /// Server communicates over streamable HTTP. - public static DiscoveredMcpServerType Http { get; } = new("http"); + /// A number. + public static McpPlanScalarValueType Number { get; } = new("number"); - /// Server communicates over Server-Sent Events (deprecated). - public static DiscoveredMcpServerType Sse { get; } = new("sse"); + /// A boolean. + public static McpPlanScalarValueType Boolean { get; } = new("boolean"); - /// Server is backed by an in-memory runtime implementation. - public static DiscoveredMcpServerType Memory { get; } = new("memory"); + /// A filesystem path. + public static McpPlanScalarValueType Path { get; } = new("path"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanScalarValueType left, McpPlanScalarValueType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanScalarValueType left, McpPlanScalarValueType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiscoveredMcpServerType other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanScalarValueType other && Equals(other); /// - public bool Equals(DiscoveredMcpServerType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPlanScalarValueType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26554,98 +28884,59 @@ public DiscoveredMcpServerType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiscoveredMcpServerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanScalarValueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiscoveredMcpServerType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanScalarValueType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredMcpServerType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScalarValueType)); } } } -/// A wire feature a caller can require of the catalog surface, negotiated per request. A grant means the runtime understands the feature's contract, not that the deployment has enabled the operation; typed unavailable results report availability separately. +/// Discriminator for an enumerated required value. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogCapability : IEquatable +public readonly struct McpPlanEnumValueType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogCapability(string value) + public McpPlanEnumValueType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Understands the current `application/mcp-server-card+json` media type. - public static CatalogCapability McpServerCard { get; } = new("mcp-server-card"); - - /// Understands the legacy `application/mcp-server+json` media type. - public static CatalogCapability LegacyMcpServerCard { get; } = new("legacy-mcp-server-card"); - - /// Understands `application/ai-skill` candidates as discovery-only and typed non-installable. - public static CatalogCapability AiSkillDiscovery { get; } = new("ai-skill-discovery"); - - /// Understands opt-in `application/vnd.github.copilot-plugin` candidates and their typed identity, version, source, and compatibility fields. - public static CatalogCapability AgentPluginDiscovery { get; } = new("agent-plugin-discovery"); - - /// Understands side-effect-free MCP install-plan requests, results, and plan handles; `planning-unavailable` separately reports that planning is not enabled. - public static CatalogCapability McpInstallPlanning { get; } = new("mcp-install-planning"); - - /// Understands plans that enumerate every eligible transport rather than a single preferred one. - public static CatalogCapability MultipleTransportChoice { get; } = new("multiple-transport-choice"); - - /// Understands explicit numbered navigation and authority-reported pagination metadata with opaque tokens. Advertised and granted only when requested. - public static CatalogCapability CatalogSearchPagination { get; } = new("catalog-search-pagination"); - - /// Understands versioned candidate trust snapshots. Protocol-3 callers must require this capability before the runtime adds the optional snapshot field. - public static CatalogCapability TrustSnapshot { get; } = new("trust-snapshot"); - - /// Understands exact candidate selection through model-safe opaque references and host-only candidate-handle hand-off. - public static CatalogCapability CatalogSelection { get; } = new("catalog-selection"); - - /// Requires an eligible credential for the selected GitHub.com account before search egress and prohibits client-side anonymous retry, including after HTTP 401 or 403. The credential is scoped to the fixed catalog authority without redirect forwarding. Neither a grant nor successful response proves that the authority accepted the identity or selected a particular backend. Preserve this requirement on every page and retry; callers omitting it retain optional authentication. - public static CatalogCapability CatalogSearchCredentialRequired { get; } = new("catalog-search-credential-required"); - - /// Captures the exact existing native session, account, host and connection for authenticated catalogue search, selection and planning. Requires catalog-search-credential-required; does not grant installation or create a session. - public static CatalogCapability CatalogSearchSessionBound { get; } = new("catalog-search-session-bound"); - - /// Understands effect-free preparation, exact human-confirmed apply and owned removal for fully resolved personal remote MCP choices without supplied inputs or configured secrets. Advertised only when the real producer and lower owned admission are linked; requires original connection and bound session authority for new work. - public static CatalogCapability McpConfirmedRemoteInstallation { get; } = new("mcp-confirmed-remote-installation"); - - /// Extends mcp-confirmed-remote-installation to declared non-secret header and URL values and receipt-owned header secrets for personal remote MCP choices. Requires mcp-confirmed-remote-installation and bound session authority. Secret values are written only to the reviewed backend after confirmation and are never returned; package and stdio choices remain unsupported. Advertised only when owned secret effects and owned secret activation are linked. - public static CatalogCapability McpConfiguredRemoteInstallation { get; } = new("mcp-configured-remote-installation"); - - /// Understands verified Agent Finder Skill install, uninstall, recovery and installation-scoped enablement APIs. Advertised only by runtimes with the Skill installation engine linked; acquisition may still be refused as feature-disabled per selected session. - public static CatalogCapability SkillConfirmedInstallation { get; } = new("skill-confirmed-installation"); + /// One of a fixed, non-empty set of permitted values. + public static McpPlanEnumValueType Enum { get; } = new("enum"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogCapability left, CatalogCapability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanEnumValueType left, McpPlanEnumValueType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogCapability left, CatalogCapability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanEnumValueType left, McpPlanEnumValueType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogCapability other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanEnumValueType other && Equals(other); /// - public bool Equals(CatalogCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPlanEnumValueType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26653,62 +28944,59 @@ public CatalogCapability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanEnumValueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogCapability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanEnumValueType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCapability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanEnumValueType)); } } } -/// Whether a planned configuration change would create or modify an entry. +/// Transport exposed by a locally launched package. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanConfigurationOperation : IEquatable +public readonly struct McpPlanPackageTransport : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanConfigurationOperation(string value) + public McpPlanPackageTransport(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Creates a configuration entry that does not exist yet. - public static McpPlanConfigurationOperation Add { get; } = new("add"); - - /// Modifies a configuration entry that already exists. - public static McpPlanConfigurationOperation Update { get; } = new("update"); + /// A locally launched process spoken to over standard input and output. + public static McpPlanPackageTransport Stdio { get; } = new("stdio"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanPackageTransport left, McpPlanPackageTransport right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanPackageTransport left, McpPlanPackageTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanConfigurationOperation other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanPackageTransport other && Equals(other); /// - public bool Equals(McpPlanConfigurationOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPlanPackageTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26716,59 +29004,65 @@ public McpPlanConfigurationOperation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanConfigurationOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanPackageTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanConfigurationOperation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanPackageTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanConfigurationOperation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPackageTransport)); } } } -/// Configuration scope an MCP install plan targets. +/// Transport exposed by a remote endpoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanScope : IEquatable +public readonly struct McpPlanRemoteTransport : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanScope(string value) + public McpPlanRemoteTransport(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The user's own MCP configuration. - public static McpPlanScope User { get; } = new("user"); + /// An HTTP endpoint. + public static McpPlanRemoteTransport Http { get; } = new("http"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanScope left, McpPlanScope right) => left.Equals(right); + /// A streamable HTTP endpoint. + public static McpPlanRemoteTransport StreamableHttp { get; } = new("streamable-http"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanScope left, McpPlanScope right) => !(left == right); + /// A server-sent events endpoint. + public static McpPlanRemoteTransport Sse { get; } = new("sse"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanRemoteTransport left, McpPlanRemoteTransport right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanRemoteTransport left, McpPlanRemoteTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanScope other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanRemoteTransport other && Equals(other); /// - public bool Equals(McpPlanScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPlanRemoteTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26776,65 +29070,62 @@ public McpPlanScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPlanRemoteTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanScope value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPlanRemoteTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanRemoteTransport)); } } } -/// What policy decided for a planned server. +/// Why capability and protocol-version negotiation refused a caller. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPolicyDecision : IEquatable +public readonly struct CatalogNegotiationRefusedReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanPolicyDecision(string value) + public CatalogNegotiationRefusedReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Policy permits the server. - public static McpPlanPolicyDecision Allowed { get; } = new("allowed"); - - /// Policy forbids the server, so the plan cannot be applied. - public static McpPlanPolicyDecision Blocked { get; } = new("blocked"); + /// The caller's protocol version is below the lowest this runtime serves. + public static CatalogNegotiationRefusedReason UnsupportedProtocolVersion { get; } = new("unsupported-protocol-version"); - /// Policy permits the server only after an explicit approval. - public static McpPlanPolicyDecision RequiresApproval { get; } = new("requires-approval"); + /// The caller requires at least one capability this runtime cannot honour. + public static CatalogNegotiationRefusedReason UnsupportedCapability { get; } = new("unsupported-capability"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanPolicyDecision other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogNegotiationRefusedReason other && Equals(other); /// - public bool Equals(McpPlanPolicyDecision other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogNegotiationRefusedReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26842,68 +29133,65 @@ public McpPlanPolicyDecision(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanPolicyDecision Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogNegotiationRefusedReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanPolicyDecision value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogNegotiationRefusedReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicyDecision)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNegotiationRefusedReason)); } } } -/// Which authority produced a policy decision. +/// Which kind of opaque handle was presented. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPolicySource : IEquatable +public readonly struct CatalogHandleType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanPolicySource(string value) + public CatalogHandleType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No policy applied, so the server is permitted by default. - public static McpPlanPolicySource None { get; } = new("none"); - - /// An enterprise allowlist evaluated the server. - public static McpPlanPolicySource EnterpriseAllowlist { get; } = new("enterprise-allowlist"); + /// A search candidate handle. + public static CatalogHandleType Candidate { get; } = new("candidate"); - /// The registry the card came from evaluated the server. - public static McpPlanPolicySource RegistryPolicy { get; } = new("registry-policy"); + /// An install plan handle. + public static CatalogHandleType Plan { get; } = new("plan"); - /// Local trust settings evaluated the server. - public static McpPlanPolicySource LocalTrust { get; } = new("local-trust"); + /// A model-safe reference to one retained search candidate. + public static CatalogHandleType Selection { get; } = new("selection"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanPolicySource left, McpPlanPolicySource right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogHandleType left, CatalogHandleType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanPolicySource left, McpPlanPolicySource right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogHandleType left, CatalogHandleType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanPolicySource other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogHandleType other && Equals(other); /// - public bool Equals(McpPlanPolicySource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogHandleType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26911,59 +29199,74 @@ public McpPlanPolicySource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanPolicySource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogHandleType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanPolicySource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogHandleType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicySource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleType)); } } } -/// Canonical digest algorithm for a validated MCP card. +/// Why a presented handle was rejected. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CardDigestAlgorithm : IEquatable +public readonly struct CatalogHandleRejectionReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CardDigestAlgorithm(string value) + public CatalogHandleRejectionReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The handle is unparseable or unknown. + public static CatalogHandleRejectionReason Invalid { get; } = new("invalid"); + + /// The handle's time to live has elapsed. + public static CatalogHandleRejectionReason Stale { get; } = new("stale"); - /// SHA-256 over RFC 8785 canonical JSON encoded as UTF-8. - public static CardDigestAlgorithm Sha256Rfc8785 { get; } = new("sha256-rfc8785"); + /// The handle has already been used, and handles are single-use. + public static CatalogHandleRejectionReason Replayed { get; } = new("replayed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CardDigestAlgorithm left, CardDigestAlgorithm right) => left.Equals(right); + /// The handle was issued by a different runtime instance or session. + public static CatalogHandleRejectionReason Foreign { get; } = new("foreign"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CardDigestAlgorithm left, CardDigestAlgorithm right) => !(left == right); + /// The handle was issued for another catalog operation. + public static CatalogHandleRejectionReason WrongKind { get; } = new("wrong-kind"); + + /// The supplied search identifier does not match the retained candidate. + public static CatalogHandleRejectionReason SearchMismatch { get; } = new("search-mismatch"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogHandleRejectionReason left, CatalogHandleRejectionReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogHandleRejectionReason left, CatalogHandleRejectionReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CardDigestAlgorithm other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogHandleRejectionReason other && Equals(other); /// - public bool Equals(CardDigestAlgorithm other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogHandleRejectionReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -26971,62 +29274,92 @@ public CardDigestAlgorithm(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CardDigestAlgorithm Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogHandleRejectionReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CardDigestAlgorithm value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogHandleRejectionReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CardDigestAlgorithm)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleRejectionReason)); } } } -/// JSON MCP card media type accepted for install planning. +/// Which request field was rejected locally or by the catalog authority. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpServerCardMediaType : IEquatable +public readonly struct CatalogInvalidRequestField : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpServerCardMediaType(string value) + public CatalogInvalidRequestField(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The current MCP server card media type. - public static McpServerCardMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); + /// The selected existing attached session was missing, malformed or unavailable. + public static CatalogInvalidRequestField PolicySessionId { get; } = new("policySessionId"); - /// The legacy MCP server card media type, accepted for compatibility. - public static McpServerCardMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); + /// The search query was empty or longer than permitted. + public static CatalogInvalidRequestField Query { get; } = new("query"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpServerCardMediaType left, McpServerCardMediaType right) => left.Equals(right); + /// The requested result count fell outside its permitted range. + public static CatalogInvalidRequestField Limit { get; } = new("limit"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpServerCardMediaType left, McpServerCardMediaType right) => !(left == right); + /// The requested candidate kinds were empty or contained a duplicate. + public static CatalogInvalidRequestField Kinds { get; } = new("kinds"); + + /// The negotiation block was missing or malformed. + public static CatalogInvalidRequestField Contract { get; } = new("contract"); + + /// The plan source was missing or malformed. + public static CatalogInvalidRequestField Source { get; } = new("source"); + + /// The supplied card was missing its media type, URL, or data. + public static CatalogInvalidRequestField Card { get; } = new("card"); + + /// The requested configuration scope is not one this runtime writes. + public static CatalogInvalidRequestField Scope { get; } = new("scope"); + + /// The pagination token or target was invalid, or the authority rejected the continuation. Repeat the search without page. + public static CatalogInvalidRequestField Page { get; } = new("page"); + + /// The locally owned session identifier was missing or malformed. + public static CatalogInvalidRequestField SessionId { get; } = new("sessionId"); + + /// The opaque selection reference was missing or malformed. + public static CatalogInvalidRequestField SelectionRef { get; } = new("selectionRef"); + + /// The terminal selection outcome was missing or unsupported. + public static CatalogInvalidRequestField Outcome { get; } = new("outcome"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpServerCardMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogInvalidRequestField other && Equals(other); /// - public bool Equals(McpServerCardMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogInvalidRequestField other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27034,71 +29367,65 @@ public McpServerCardMediaType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpServerCardMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogInvalidRequestField Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpServerCardMediaType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogInvalidRequestField value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpServerCardMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogInvalidRequestField)); } } } -/// Where a required value is applied when the planned server is launched. +/// Why the catalog authority did not accept the caller's identity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanValueCategory : IEquatable +public readonly struct CatalogAuthenticationRequiredReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanValueCategory(string value) + public CatalogAuthenticationRequiredReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Set as an environment variable on the launched process. - public static McpPlanValueCategory EnvironmentVariable { get; } = new("environment-variable"); - - /// Passed to the runtime that launches the package. - public static McpPlanValueCategory RuntimeArgument { get; } = new("runtime-argument"); - - /// Passed to the packaged server itself. - public static McpPlanValueCategory PackageArgument { get; } = new("package-argument"); + /// No credential was presented, so there is nothing to refresh and the caller must sign in. + public static CatalogAuthenticationRequiredReason NoCredential { get; } = new("no-credential"); - /// Sent as a request header to a remote endpoint. - public static McpPlanValueCategory Header { get; } = new("header"); + /// A credential was presented and its lifetime has elapsed. A silent refresh is worth attempting before prompting anyone. + public static CatalogAuthenticationRequiredReason CredentialExpired { get; } = new("credential-expired"); - /// Substituted into the remote endpoint URL. - public static McpPlanValueCategory UrlVariable { get; } = new("url-variable"); + /// A credential was presented and the authority refused it, for example because it was revoked, malformed, or issued for another audience. Refreshing the same rejected credential is not useful; the caller must sign in again. + public static CatalogAuthenticationRequiredReason CredentialRejected { get; } = new("credential-rejected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanValueCategory left, McpPlanValueCategory right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanValueCategory left, McpPlanValueCategory right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanValueCategory other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAuthenticationRequiredReason other && Equals(other); /// - public bool Equals(McpPlanValueCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogAuthenticationRequiredReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27106,68 +29433,89 @@ public McpPlanValueCategory(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanValueCategory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogAuthenticationRequiredReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanValueCategory value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogAuthenticationRequiredReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanValueCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAuthenticationRequiredReason)); } } } -/// Scalar type a required value must conform to. +/// Categorised network failure, low cardinality so it can be aggregated without carrying a URL. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanScalarValueType : IEquatable +public readonly struct CatalogNetworkFailureReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanScalarValueType(string value) + public CatalogNetworkFailureReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Free text. - public static McpPlanScalarValueType String { get; } = new("string"); + /// No network is available, so nothing was attempted. + public static CatalogNetworkFailureReason Offline { get; } = new("offline"); - /// A number. - public static McpPlanScalarValueType Number { get; } = new("number"); + /// The authority's name could not be resolved. + public static CatalogNetworkFailureReason Dns { get; } = new("dns"); - /// A boolean. - public static McpPlanScalarValueType Boolean { get; } = new("boolean"); + /// The request exceeded its time budget. + public static CatalogNetworkFailureReason Timeout { get; } = new("timeout"); - /// A filesystem path. - public static McpPlanScalarValueType Path { get; } = new("path"); + /// The TLS handshake or certificate validation failed. + public static CatalogNetworkFailureReason Tls { get; } = new("tls"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanScalarValueType left, McpPlanScalarValueType right) => left.Equals(right); + /// The connection was refused or reset. + public static CatalogNetworkFailureReason ConnectionRefused { get; } = new("connection-refused"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanScalarValueType left, McpPlanScalarValueType right) => !(left == right); + /// The configured proxy returned 407 and requires authentication. + public static CatalogNetworkFailureReason ProxyAuthenticationRequired { get; } = new("proxy-authentication-required"); + + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + public static CatalogNetworkFailureReason RateLimited { get; } = new("rate-limited"); + + /// The authority returned a transient 5xx response. + public static CatalogNetworkFailureReason ServiceUnavailable { get; } = new("service-unavailable"); + + /// The authority returned another status the runtime treats as a failure. + public static CatalogNetworkFailureReason HttpStatus { get; } = new("http-status"); + + /// The response exceeded the permitted size. + public static CatalogNetworkFailureReason ResponseTooLarge { get; } = new("response-too-large"); + + /// A redirect was refused by the runtime's redirect policy. + public static CatalogNetworkFailureReason RedirectRejected { get; } = new("redirect-rejected"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogNetworkFailureReason left, CatalogNetworkFailureReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogNetworkFailureReason left, CatalogNetworkFailureReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanScalarValueType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogNetworkFailureReason other && Equals(other); /// - public bool Equals(McpPlanScalarValueType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogNetworkFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27175,59 +29523,74 @@ public McpPlanScalarValueType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanScalarValueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogNetworkFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanScalarValueType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogNetworkFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScalarValueType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNetworkFailureReason)); } } } -/// Discriminator for an enumerated required value. +/// Which hardened-fetch control refused a retrieval. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanEnumValueType : IEquatable +public readonly struct CatalogUnsafeRetrievalReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanEnumValueType(string value) + public CatalogUnsafeRetrievalReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// One of a fixed, non-empty set of permitted values. - public static McpPlanEnumValueType Enum { get; } = new("enum"); + /// The URL used a scheme the runtime refuses to fetch. + public static CatalogUnsafeRetrievalReason BlockedScheme { get; } = new("blocked-scheme"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanEnumValueType left, McpPlanEnumValueType right) => left.Equals(right); + /// The URL embedded credentials. + public static CatalogUnsafeRetrievalReason CredentialsInUrl { get; } = new("credentials-in-url"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanEnumValueType left, McpPlanEnumValueType right) => !(left == right); + /// The URL resolved to a loopback, private, link-local, or cloud metadata address. + public static CatalogUnsafeRetrievalReason BlockedAddress { get; } = new("blocked-address"); + + /// A redirect target resolved to a blocked address. + public static CatalogUnsafeRetrievalReason RedirectToBlockedAddress { get; } = new("redirect-to-blocked-address"); + + /// The configured proxy policy refused the request. + public static CatalogUnsafeRetrievalReason ProxyRejected { get; } = new("proxy-rejected"); + + /// The authority is not permitted for card retrieval. + public static CatalogUnsafeRetrievalReason HostNotPermitted { get; } = new("host-not-permitted"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanEnumValueType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnsafeRetrievalReason other && Equals(other); /// - public bool Equals(McpPlanEnumValueType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogUnsafeRetrievalReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27235,59 +29598,68 @@ public McpPlanEnumValueType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanEnumValueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogUnsafeRetrievalReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanEnumValueType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogUnsafeRetrievalReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanEnumValueType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnsafeRetrievalReason)); } } } -/// Transport exposed by a locally launched package. +/// Media type a catalog card is interpreted as. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPackageTransport : IEquatable +public readonly struct CatalogMediaType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanPackageTransport(string value) + public CatalogMediaType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A locally launched process spoken to over standard input and output. - public static McpPlanPackageTransport Stdio { get; } = new("stdio"); + /// The current MCP server card media type. + public static CatalogMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanPackageTransport left, McpPlanPackageTransport right) => left.Equals(right); + /// The legacy MCP server card media type, accepted for compatibility. + public static CatalogMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanPackageTransport left, McpPlanPackageTransport right) => !(left == right); + /// An AI skill card. Representable and searchable, but typed non-installable. + public static CatalogMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); + + /// An inert Agent Plugin descriptor. + public static CatalogMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogMediaType left, CatalogMediaType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogMediaType left, CatalogMediaType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanPackageTransport other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMediaType other && Equals(other); /// - public bool Equals(McpPlanPackageTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27295,65 +29667,71 @@ public McpPlanPackageTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanPackageTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanPackageTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPackageTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMediaType)); } } } -/// Transport exposed by a remote endpoint. +/// How a card failed validation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanRemoteTransport : IEquatable +public readonly struct CatalogMalformedCardReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpPlanRemoteTransport(string value) + public CatalogMalformedCardReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// An HTTP endpoint. - public static McpPlanRemoteTransport Http { get; } = new("http"); + /// The document is not well-formed JSON. + public static CatalogMalformedCardReason InvalidJson { get; } = new("invalid-json"); - /// A streamable HTTP endpoint. - public static McpPlanRemoteTransport StreamableHttp { get; } = new("streamable-http"); + /// The document does not satisfy its media type's schema. + public static CatalogMalformedCardReason SchemaViolation { get; } = new("schema-violation"); - /// A server-sent events endpoint. - public static McpPlanRemoteTransport Sse { get; } = new("sse"); + /// The declared media type is not one this runtime understands. + public static CatalogMalformedCardReason UnsupportedMediaType { get; } = new("unsupported-media-type"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanRemoteTransport left, McpPlanRemoteTransport right) => left.Equals(right); + /// A field the media type requires is absent. + public static CatalogMalformedCardReason MissingRequiredField { get; } = new("missing-required-field"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanRemoteTransport left, McpPlanRemoteTransport right) => !(left == right); + /// The document exceeded the permitted size. + public static CatalogMalformedCardReason SizeLimitExceeded { get; } = new("size-limit-exceeded"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpPlanRemoteTransport other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMalformedCardReason other && Equals(other); /// - public bool Equals(McpPlanRemoteTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogMalformedCardReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27361,62 +29739,68 @@ public McpPlanRemoteTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpPlanRemoteTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogMalformedCardReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpPlanRemoteTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogMalformedCardReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanRemoteTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMalformedCardReason)); } } } -/// Why capability and protocol-version negotiation refused a caller. +/// Which wire-contract rule an upstream response broke. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNegotiationRefusedReason : IEquatable +public readonly struct CatalogContractViolationReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogNegotiationRefusedReason(string value) + public CatalogContractViolationReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The caller's protocol version is below the lowest this runtime serves. - public static CatalogNegotiationRefusedReason UnsupportedProtocolVersion { get; } = new("unsupported-protocol-version"); + /// A result carried both a URL and embedded data, when exactly one is permitted. + public static CatalogContractViolationReason BothUrlAndData { get; } = new("both-url-and-data"); - /// The caller requires at least one capability this runtime cannot honour. - public static CatalogNegotiationRefusedReason UnsupportedCapability { get; } = new("unsupported-capability"); + /// A result carried neither a URL nor embedded data, when exactly one is required. + public static CatalogContractViolationReason NeitherUrlNorData { get; } = new("neither-url-nor-data"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => left.Equals(right); + /// Two results claimed the same collision key: normalised identity for existing kinds, or normalised identity and declared version for Agent Plugins. + public static CatalogContractViolationReason DuplicateIdentity { get; } = new("duplicate-identity"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => !(left == right); + /// A result declared no media type, or one this contract does not model. + public static CatalogContractViolationReason UnknownMediaType { get; } = new("unknown-media-type"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogContractViolationReason left, CatalogContractViolationReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogContractViolationReason left, CatalogContractViolationReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogNegotiationRefusedReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogContractViolationReason other && Equals(other); /// - public bool Equals(CatalogNegotiationRefusedReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogContractViolationReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27424,65 +29808,65 @@ public CatalogNegotiationRefusedReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogNegotiationRefusedReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogContractViolationReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogNegotiationRefusedReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogContractViolationReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNegotiationRefusedReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogContractViolationReason)); } } } -/// Which kind of opaque handle was presented. +/// Why no usable transport could be offered. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogHandleType : IEquatable +public readonly struct CatalogUnavailableTransportReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogHandleType(string value) + public CatalogUnavailableTransportReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A search candidate handle. - public static CatalogHandleType Candidate { get; } = new("candidate"); + /// The card advertises no transport this runtime can use. + public static CatalogUnavailableTransportReason NoEligibleTransport { get; } = new("no-eligible-transport"); - /// An install plan handle. - public static CatalogHandleType Plan { get; } = new("plan"); + /// Every advertised transport is of a kind this runtime does not implement. + public static CatalogUnavailableTransportReason TransportNotSupported { get; } = new("transport-not-supported"); - /// A model-safe reference to one retained search candidate. - public static CatalogHandleType Selection { get; } = new("selection"); + /// Eligible remotes could not be enumerated, so no explicit choice can be offered. + public static CatalogUnavailableTransportReason RemoteEnumerationUnavailable { get; } = new("remote-enumeration-unavailable"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogHandleType left, CatalogHandleType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogHandleType left, CatalogHandleType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogHandleType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnavailableTransportReason other && Equals(other); /// - public bool Equals(CatalogHandleType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogUnavailableTransportReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27490,74 +29874,65 @@ public CatalogHandleType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogHandleType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogUnavailableTransportReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogHandleType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogUnavailableTransportReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableTransportReason)); } } } -/// Why a presented handle was rejected. +/// Why a discoverable candidate cannot be installed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogHandleRejectionReason : IEquatable +public readonly struct CatalogNotInstallableReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogHandleRejectionReason(string value) + public CatalogNotInstallableReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The handle is unparseable or unknown. - public static CatalogHandleRejectionReason Invalid { get; } = new("invalid"); - - /// The handle's time to live has elapsed. - public static CatalogHandleRejectionReason Stale { get; } = new("stale"); - - /// The handle has already been used, and handles are single-use. - public static CatalogHandleRejectionReason Replayed { get; } = new("replayed"); - - /// The handle was issued by a different runtime instance or session. - public static CatalogHandleRejectionReason Foreign { get; } = new("foreign"); - - /// The handle was issued for another catalog operation. - public static CatalogHandleRejectionReason WrongKind { get; } = new("wrong-kind"); + /// This kind of resource is not installable through this surface. + public static CatalogNotInstallableReason KindNotInstallable { get; } = new("kind-not-installable"); - /// The supplied search identifier does not match the retained candidate. - public static CatalogHandleRejectionReason SearchMismatch { get; } = new("search-mismatch"); + /// AI skills are discoverable but have no typed importer in this phase. + public static CatalogNotInstallableReason AiSkillNotInstallable { get; } = new("ai-skill-not-installable"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogHandleRejectionReason left, CatalogHandleRejectionReason right) => left.Equals(right); + /// Policy forbids installing this candidate. + public static CatalogNotInstallableReason PolicyForbids { get; } = new("policy-forbids"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogHandleRejectionReason left, CatalogHandleRejectionReason right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogHandleRejectionReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogNotInstallableReason other && Equals(other); /// - public bool Equals(CatalogHandleRejectionReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogNotInstallableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27565,92 +29940,71 @@ public CatalogHandleRejectionReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogHandleRejectionReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogNotInstallableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogHandleRejectionReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogNotInstallableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleRejectionReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNotInstallableReason)); } } } -/// Which request field was rejected locally or by the catalog authority. +/// Why a catalog operation is not available on this runtime. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogInvalidRequestField : IEquatable +public readonly struct CatalogUnavailableReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogInvalidRequestField(string value) + public CatalogUnavailableReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The selected existing attached session was missing, malformed or unavailable. - public static CatalogInvalidRequestField PolicySessionId { get; } = new("policySessionId"); - - /// The search query was empty or longer than permitted. - public static CatalogInvalidRequestField Query { get; } = new("query"); - - /// The requested result count fell outside its permitted range. - public static CatalogInvalidRequestField Limit { get; } = new("limit"); - - /// The requested candidate kinds were empty or contained a duplicate. - public static CatalogInvalidRequestField Kinds { get; } = new("kinds"); - - /// The negotiation block was missing or malformed. - public static CatalogInvalidRequestField Contract { get; } = new("contract"); - - /// The plan source was missing or malformed. - public static CatalogInvalidRequestField Source { get; } = new("source"); - - /// The supplied card was missing its media type, URL, or data. - public static CatalogInvalidRequestField Card { get; } = new("card"); - - /// The requested configuration scope is not one this runtime writes. - public static CatalogInvalidRequestField Scope { get; } = new("scope"); + /// Bounded search is not wired up on this runtime build. + public static CatalogUnavailableReason SearchUnavailable { get; } = new("search-unavailable"); - /// The pagination token or target was invalid, or the authority rejected the continuation. Repeat the search without page. - public static CatalogInvalidRequestField Page { get; } = new("page"); + /// Install planning is not wired up on this runtime build. + public static CatalogUnavailableReason PlanningUnavailable { get; } = new("planning-unavailable"); - /// The locally owned session identifier was missing or malformed. - public static CatalogInvalidRequestField SessionId { get; } = new("sessionId"); + /// Exact candidate selection is not available in this session or runtime. + public static CatalogUnavailableReason SelectionUnavailable { get; } = new("selection-unavailable"); - /// The opaque selection reference was missing or malformed. - public static CatalogInvalidRequestField SelectionRef { get; } = new("selectionRef"); + /// No catalog authority is configured for this runtime. + public static CatalogUnavailableReason AuthorityNotConfigured { get; } = new("authority-not-configured"); - /// The terminal selection outcome was missing or unsupported. - public static CatalogInvalidRequestField Outcome { get; } = new("outcome"); + /// The surface is disabled by policy on this runtime. + public static CatalogUnavailableReason DisabledByPolicy { get; } = new("disabled-by-policy"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogUnavailableReason left, CatalogUnavailableReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogInvalidRequestField left, CatalogInvalidRequestField right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogUnavailableReason left, CatalogUnavailableReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogInvalidRequestField other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnavailableReason other && Equals(other); /// - public bool Equals(CatalogInvalidRequestField other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogUnavailableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27658,65 +30012,68 @@ public CatalogInvalidRequestField(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogInvalidRequestField Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogUnavailableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogInvalidRequestField value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogUnavailableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogInvalidRequestField)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableReason)); } } } -/// Why the catalog authority did not accept the caller's identity. +/// Authority-computed exposure eligibility, kept separate from tier. The current tier-only Agent Finder response maps to `unknown`, never to a locally inferred eligibility. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAuthenticationRequiredReason : IEquatable +public readonly struct CatalogTrustEligibility : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogAuthenticationRequiredReason(string value) + public CatalogTrustEligibility(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No credential was presented, so there is nothing to refresh and the caller must sign in. - public static CatalogAuthenticationRequiredReason NoCredential { get; } = new("no-credential"); + /// Eligible for default catalogue exposure. + public static CatalogTrustEligibility Default { get; } = new("default"); - /// A credential was presented and its lifetime has elapsed. A silent refresh is worth attempting before prompting anyone. - public static CatalogAuthenticationRequiredReason CredentialExpired { get; } = new("credential-expired"); + /// Eligible only when expanded or community results are requested. + public static CatalogTrustEligibility Expanded { get; } = new("expanded"); - /// A credential was presented and the authority refused it, for example because it was revoked, malformed, or issued for another audience. Refreshing the same rejected credential is not useful; the caller must sign in again. - public static CatalogAuthenticationRequiredReason CredentialRejected { get; } = new("credential-rejected"); + /// Not eligible for normal catalogue exposure. + public static CatalogTrustEligibility Hidden { get; } = new("hidden"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason right) => left.Equals(right); + /// The authority did not supply an eligibility decision. + public static CatalogTrustEligibility Unknown { get; } = new("unknown"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogTrustEligibility left, CatalogTrustEligibility right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogTrustEligibility left, CatalogTrustEligibility right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAuthenticationRequiredReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustEligibility other && Equals(other); /// - public bool Equals(CatalogAuthenticationRequiredReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogTrustEligibility other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27724,89 +30081,59 @@ public CatalogAuthenticationRequiredReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogAuthenticationRequiredReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogTrustEligibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogAuthenticationRequiredReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogTrustEligibility value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAuthenticationRequiredReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustEligibility)); } } } -/// Categorised network failure, low cardinality so it can be aggregated without carrying a URL. +/// Bounded authority that supplied a catalogue trust observation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNetworkFailureReason : IEquatable +public readonly struct CatalogTrustSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogNetworkFailureReason(string value) + public CatalogTrustSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No network is available, so nothing was attempted. - public static CatalogNetworkFailureReason Offline { get; } = new("offline"); - - /// The authority's name could not be resolved. - public static CatalogNetworkFailureReason Dns { get; } = new("dns"); - - /// The request exceeded its time budget. - public static CatalogNetworkFailureReason Timeout { get; } = new("timeout"); - - /// The TLS handshake or certificate validation failed. - public static CatalogNetworkFailureReason Tls { get; } = new("tls"); - - /// The connection was refused or reset. - public static CatalogNetworkFailureReason ConnectionRefused { get; } = new("connection-refused"); - - /// The configured proxy returned 407 and requires authentication. - public static CatalogNetworkFailureReason ProxyAuthenticationRequired { get; } = new("proxy-authentication-required"); - - /// The authority rate-limited requests and supplied or implied a bounded cooldown. - public static CatalogNetworkFailureReason RateLimited { get; } = new("rate-limited"); - - /// The authority returned a transient 5xx response. - public static CatalogNetworkFailureReason ServiceUnavailable { get; } = new("service-unavailable"); - - /// The authority returned another status the runtime treats as a failure. - public static CatalogNetworkFailureReason HttpStatus { get; } = new("http-status"); - - /// The response exceeded the permitted size. - public static CatalogNetworkFailureReason ResponseTooLarge { get; } = new("response-too-large"); - - /// A redirect was refused by the runtime's redirect policy. - public static CatalogNetworkFailureReason RedirectRejected { get; } = new("redirect-rejected"); + /// GitHub Agent Finder supplied the trust field on its search result. + public static CatalogTrustSource AgentFinder { get; } = new("agent-finder"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogNetworkFailureReason left, CatalogNetworkFailureReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogTrustSource left, CatalogTrustSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogNetworkFailureReason left, CatalogNetworkFailureReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogTrustSource left, CatalogTrustSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogNetworkFailureReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustSource other && Equals(other); /// - public bool Equals(CatalogNetworkFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogTrustSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27814,74 +30141,59 @@ public CatalogNetworkFailureReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogNetworkFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogTrustSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogNetworkFailureReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogTrustSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNetworkFailureReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSource)); } } } -/// Which hardened-fetch control refused a retrieval. +/// Schema version of the catalogue trust snapshot envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnsafeRetrievalReason : IEquatable +public readonly struct CatalogTrustSnapshotSchemaVersion : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogUnsafeRetrievalReason(string value) + public CatalogTrustSnapshotSchemaVersion(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The URL used a scheme the runtime refuses to fetch. - public static CatalogUnsafeRetrievalReason BlockedScheme { get; } = new("blocked-scheme"); - - /// The URL embedded credentials. - public static CatalogUnsafeRetrievalReason CredentialsInUrl { get; } = new("credentials-in-url"); - - /// The URL resolved to a loopback, private, link-local, or cloud metadata address. - public static CatalogUnsafeRetrievalReason BlockedAddress { get; } = new("blocked-address"); - - /// A redirect target resolved to a blocked address. - public static CatalogUnsafeRetrievalReason RedirectToBlockedAddress { get; } = new("redirect-to-blocked-address"); - - /// The configured proxy policy refused the request. - public static CatalogUnsafeRetrievalReason ProxyRejected { get; } = new("proxy-rejected"); - - /// The authority is not permitted for card retrieval. - public static CatalogUnsafeRetrievalReason HostNotPermitted { get; } = new("host-not-permitted"); + /// Initial envelope carrying one bounded service tier or one explicit unavailable state. + public static CatalogTrustSnapshotSchemaVersion V1 { get; } = new("v1"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogUnsafeRetrievalReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustSnapshotSchemaVersion other && Equals(other); /// - public bool Equals(CatalogUnsafeRetrievalReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogTrustSnapshotSchemaVersion other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27889,68 +30201,62 @@ public CatalogUnsafeRetrievalReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogUnsafeRetrievalReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogTrustSnapshotSchemaVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogUnsafeRetrievalReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogTrustSnapshotSchemaVersion value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnsafeRetrievalReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSnapshotSchemaVersion)); } } } -/// Media type a catalog card is interpreted as. +/// Service-computed trust tier currently emitted by Agent Finder. It is independent of search score, popularity, and client-side ranking. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogMediaType : IEquatable +public readonly struct CatalogTrustTier : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogMediaType(string value) + public CatalogTrustTier(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The current MCP server card media type. - public static CatalogMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); - - /// The legacy MCP server card media type, accepted for compatibility. - public static CatalogMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); - - /// An AI skill card. Representable and searchable, but typed non-installable. - public static CatalogMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); + /// Tier one as assigned by the catalogue authority. + public static CatalogTrustTier T1 { get; } = new("T1"); - /// An inert Agent Plugin descriptor. - public static CatalogMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); + /// Tier two as assigned by the catalogue authority. + public static CatalogTrustTier T2 { get; } = new("T2"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogMediaType left, CatalogMediaType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogTrustTier left, CatalogTrustTier right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogMediaType left, CatalogMediaType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogTrustTier left, CatalogTrustTier right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustTier other && Equals(other); /// - public bool Equals(CatalogMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogTrustTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -27958,71 +30264,74 @@ public CatalogMediaType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogTrustTier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogMediaType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogTrustTier value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustTier)); } } } -/// How a card failed validation. +/// Configuration ownership and setup observations, distinct from tool permissions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogMalformedCardReason : IEquatable +public readonly struct McpInstallationState : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogMalformedCardReason(string value) + public McpInstallationState(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The document is not well-formed JSON. - public static CatalogMalformedCardReason InvalidJson { get; } = new("invalid-json"); + /// Owned configuration exists; inventory alone does not grant activation. + public static McpInstallationState NeedsSetup { get; } = new("needs-setup"); - /// The document does not satisfy its media type's schema. - public static CatalogMalformedCardReason SchemaViolation { get; } = new("schema-violation"); + /// The selected authorised session reports an active installation. + public static McpInstallationState Active { get; } = new("active"); - /// The declared media type is not one this runtime understands. - public static CatalogMalformedCardReason UnsupportedMediaType { get; } = new("unsupported-media-type"); + /// The selected server requires explicit sign-in. + public static McpInstallationState AuthenticationRequired { get; } = new("authentication-required"); - /// A field the media type requires is absent. - public static CatalogMalformedCardReason MissingRequiredField { get; } = new("missing-required-field"); + /// The selected server could not be activated. + public static McpInstallationState ActivationFailed { get; } = new("activation-failed"); - /// The document exceeded the permitted size. - public static CatalogMalformedCardReason SizeLimitExceeded { get; } = new("size-limit-exceeded"); + /// Owned configuration no longer matches its receipt. + public static McpInstallationState ConfigurationModified { get; } = new("configuration-modified"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => left.Equals(right); + /// Confirmed durable work or unsafe evidence requires recovery. + public static McpInstallationState RecoveryRequired { get; } = new("recovery-required"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpInstallationState left, McpInstallationState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpInstallationState left, McpInstallationState right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogMalformedCardReason other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationState other && Equals(other); /// - public bool Equals(CatalogMalformedCardReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpInstallationState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28030,68 +30339,125 @@ public CatalogMalformedCardReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogMalformedCardReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpInstallationState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogMalformedCardReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpInstallationState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMalformedCardReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationState)); } } } -/// Which wire-contract rule an upstream response broke. +/// Bounded refusal categories, without echoing handles, credentials or configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogContractViolationReason : IEquatable +public readonly struct McpInstallationFailureReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogContractViolationReason(string value) + public McpInstallationFailureReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A result carried both a URL and embedded data, when exactly one is permitted. - public static CatalogContractViolationReason BothUrlAndData { get; } = new("both-url-and-data"); + /// The selected choice or request is unsupported or malformed. + public static McpInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - /// A result carried neither a URL nor embedded data, when exactly one is required. - public static CatalogContractViolationReason NeitherUrlNorData { get; } = new("neither-url-nor-data"); + /// The bounded original-connection operation limit was reached. + public static McpInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - /// Two results claimed the same collision key: normalised identity for existing kinds, or normalised identity and declared version for Agent Plugins. - public static CatalogContractViolationReason DuplicateIdentity { get; } = new("duplicate-identity"); + /// The original operation was cancelled. + public static McpInstallationFailureReason Cancelled { get; } = new("cancelled"); - /// A result declared no media type, or one this contract does not model. - public static CatalogContractViolationReason UnknownMediaType { get; } = new("unknown-media-type"); + /// Required installation capabilities were omitted. + public static McpInstallationFailureReason CapabilityRequired { get; } = new("capability-required"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogContractViolationReason left, CatalogContractViolationReason right) => left.Equals(right); + /// The original host cannot receive human confirmation. + public static McpInstallationFailureReason ConfirmationUnavailable { get; } = new("confirmation-unavailable"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogContractViolationReason left, CatalogContractViolationReason right) => !(left == right); + /// The confirmation response is malformed or mismatched. + public static McpInstallationFailureReason ConfirmationInvalid { get; } = new("confirmation-invalid"); + + /// Existing authenticated session and host authority is unavailable. + public static McpInstallationFailureReason PolicyContextUnavailable { get; } = new("policy-context-unavailable"); + + /// The original authority or policy changed. + public static McpInstallationFailureReason PolicyChanged { get; } = new("policy-changed"); + + /// Current managed policy refuses the operation. + public static McpInstallationFailureReason PolicyDenied { get; } = new("policy-denied"); + + /// Configuration changed after the reviewed snapshot. + public static McpInstallationFailureReason ConfigurationChanged { get; } = new("configuration-changed"); + + /// Installed configuration no longer matches ownership evidence. + public static McpInstallationFailureReason ConfigurationModified { get; } = new("configuration-modified"); + + /// No matching owned resource or original operation exists. + public static McpInstallationFailureReason ResourceNotFound { get; } = new("resource-not-found"); + + /// The original plan deadline elapsed. + public static McpInstallationFailureReason PlanExpired { get; } = new("plan-expired"); + + /// The one-use plan or prepared operation was already consumed. + public static McpInstallationFailureReason PlanReplayed { get; } = new("plan-replayed"); + + /// The handle belongs to a different runtime, session or connection. + public static McpInstallationFailureReason ForeignRuntime { get; } = new("foreign-runtime"); + + /// Fresh bound planning is required. + public static McpInstallationFailureReason ReplanRequired { get; } = new("replan-required"); + + /// Exact original source revalidation is unsupported. + public static McpInstallationFailureReason SourceRevalidationUnavailable { get; } = new("source-revalidation-unavailable"); + + /// The source differs from the retained commitment. + public static McpInstallationFailureReason SourceChanged { get; } = new("source-changed"); + + /// The original source could not be retrieved safely. + public static McpInstallationFailureReason SourceUnavailable { get; } = new("source-unavailable"); + + /// Authoritative Registry interpretation is unavailable. + public static McpInstallationFailureReason RegistryUnavailable { get; } = new("registry-unavailable"); + + /// The selected secret backend is unavailable. + public static McpInstallationFailureReason SecretStoreUnavailable { get; } = new("secret-store-unavailable"); + + /// Required owned admission and lifecycle support is absent. + public static McpInstallationFailureReason LifecycleUnavailable { get; } = new("lifecycle-unavailable"); + + /// A storage operation failed; inspect any allocated operation before retrying. + public static McpInstallationFailureReason WriteFailed { get; } = new("write-failed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpInstallationFailureReason left, McpInstallationFailureReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpInstallationFailureReason left, McpInstallationFailureReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogContractViolationReason other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationFailureReason other && Equals(other); /// - public bool Equals(CatalogContractViolationReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpInstallationFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28099,65 +30465,62 @@ public CatalogContractViolationReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogContractViolationReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpInstallationFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogContractViolationReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpInstallationFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogContractViolationReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationFailureReason)); } } } -/// Why no usable transport could be offered. +/// Explicit backend selection is part of the final review; failures never switch backends. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnavailableTransportReason : IEquatable +public readonly struct McpInstallationSecretStorage : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogUnavailableTransportReason(string value) + public McpInstallationSecretStorage(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The card advertises no transport this runtime can use. - public static CatalogUnavailableTransportReason NoEligibleTransport { get; } = new("no-eligible-transport"); - - /// Every advertised transport is of a kind this runtime does not implement. - public static CatalogUnavailableTransportReason TransportNotSupported { get; } = new("transport-not-supported"); - - /// Eligible remotes could not be enumerated, so no explicit choice can be offered. - public static CatalogUnavailableTransportReason RemoteEnumerationUnavailable { get; } = new("remote-enumeration-unavailable"); + /// The selected operating-system keychain, without fallback to file storage. + public static McpInstallationSecretStorage Keychain { get; } = new("keychain"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason right) => left.Equals(right); + /// The explicitly selected private file backend. + public static McpInstallationSecretStorage PrivateFile { get; } = new("private-file"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpInstallationSecretStorage left, McpInstallationSecretStorage right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpInstallationSecretStorage left, McpInstallationSecretStorage right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogUnavailableTransportReason other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationSecretStorage other && Equals(other); /// - public bool Equals(CatalogUnavailableTransportReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpInstallationSecretStorage other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28165,65 +30528,62 @@ public CatalogUnavailableTransportReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogUnavailableTransportReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpInstallationSecretStorage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogUnavailableTransportReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpInstallationSecretStorage value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableTransportReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationSecretStorage)); } } } -/// Why a discoverable candidate cannot be installed. +/// Persisted extension discovery source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNotInstallableReason : IEquatable +public readonly struct DiscoveredExtensionSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogNotInstallableReason(string value) + public DiscoveredExtensionSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// This kind of resource is not installable through this surface. - public static CatalogNotInstallableReason KindNotInstallable { get; } = new("kind-not-installable"); - - /// AI skills are discoverable but have no typed importer in this phase. - public static CatalogNotInstallableReason AiSkillNotInstallable { get; } = new("ai-skill-not-installable"); + /// Extension discovered from the user's extensions directory. + public static DiscoveredExtensionSource User { get; } = new("user"); - /// Policy forbids installing this candidate. - public static CatalogNotInstallableReason PolicyForbids { get; } = new("policy-forbids"); + /// Extension contributed by an installed plugin. + public static DiscoveredExtensionSource Plugin { get; } = new("plugin"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiscoveredExtensionSource left, DiscoveredExtensionSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiscoveredExtensionSource left, DiscoveredExtensionSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogNotInstallableReason other && Equals(other); + public override bool Equals(object? obj) => obj is DiscoveredExtensionSource other && Equals(other); /// - public bool Equals(CatalogNotInstallableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiscoveredExtensionSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28231,71 +30591,65 @@ public CatalogNotInstallableReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogNotInstallableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiscoveredExtensionSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogNotInstallableReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiscoveredExtensionSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNotInstallableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionSource)); } } } -/// Why a catalog operation is not available on this runtime. +/// Effective extension loading and agent-management mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnavailableReason : IEquatable +public readonly struct DiscoveredExtensionMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogUnavailableReason(string value) + public DiscoveredExtensionMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Bounded search is not wired up on this runtime build. - public static CatalogUnavailableReason SearchUnavailable { get; } = new("search-unavailable"); - - /// Install planning is not wired up on this runtime build. - public static CatalogUnavailableReason PlanningUnavailable { get; } = new("planning-unavailable"); - - /// Exact candidate selection is not available in this session or runtime. - public static CatalogUnavailableReason SelectionUnavailable { get; } = new("selection-unavailable"); + /// Extensions are not loaded. + public static DiscoveredExtensionMode Disabled { get; } = new("disabled"); - /// No catalog authority is configured for this runtime. - public static CatalogUnavailableReason AuthorityNotConfigured { get; } = new("authority-not-configured"); + /// Extensions are loaded, but the agent cannot create, reload, or manage them. + public static DiscoveredExtensionMode LoadOnly { get; } = new("load_only"); - /// The surface is disabled by policy on this runtime. - public static CatalogUnavailableReason DisabledByPolicy { get; } = new("disabled-by-policy"); + /// Extensions are loaded and the agent can create, reload, and manage them. + public static DiscoveredExtensionMode LoadAndAugment { get; } = new("load_and_augment"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogUnavailableReason left, CatalogUnavailableReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiscoveredExtensionMode left, DiscoveredExtensionMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogUnavailableReason left, CatalogUnavailableReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiscoveredExtensionMode left, DiscoveredExtensionMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogUnavailableReason other && Equals(other); + public override bool Equals(object? obj) => obj is DiscoveredExtensionMode other && Equals(other); /// - public bool Equals(CatalogUnavailableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiscoveredExtensionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28303,68 +30657,59 @@ public CatalogUnavailableReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogUnavailableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiscoveredExtensionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogUnavailableReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiscoveredExtensionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionMode)); } } } -/// Authority-computed exposure eligibility, kept separate from tier. The current tier-only Agent Finder response maps to `unknown`, never to a locally inferred eligibility. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustEligibility : IEquatable +public readonly struct SkillInstallationScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogTrustEligibility(string value) + public SkillInstallationScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Eligible for default catalogue exposure. - public static CatalogTrustEligibility Default { get; } = new("default"); - - /// Eligible only when expanded or community results are requested. - public static CatalogTrustEligibility Expanded { get; } = new("expanded"); - - /// Not eligible for normal catalogue exposure. - public static CatalogTrustEligibility Hidden { get; } = new("hidden"); - - /// The authority did not supply an eligibility decision. - public static CatalogTrustEligibility Unknown { get; } = new("unknown"); + /// The user's personal Copilot home. + public static SkillInstallationScope Personal { get; } = new("personal"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogTrustEligibility left, CatalogTrustEligibility right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillInstallationScope left, SkillInstallationScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogTrustEligibility left, CatalogTrustEligibility right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInstallationScope left, SkillInstallationScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogTrustEligibility other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationScope other && Equals(other); /// - public bool Equals(CatalogTrustEligibility other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SkillInstallationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28372,59 +30717,65 @@ public CatalogTrustEligibility(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogTrustEligibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SkillInstallationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogTrustEligibility value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SkillInstallationScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustEligibility)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationScope)); } } } -/// Bounded authority that supplied a catalogue trust observation. +/// Owned Skill state observed from files and receipts. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustSource : IEquatable +public readonly struct SkillInstallationOwnershipState : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogTrustSource(string value) + public SkillInstallationOwnershipState(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// GitHub Agent Finder supplied the trust field on its search result. - public static CatalogTrustSource AgentFinder { get; } = new("agent-finder"); + /// Owned files and receipt evidence match. + public static SkillInstallationOwnershipState Intact { get; } = new("intact"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogTrustSource left, CatalogTrustSource right) => left.Equals(right); + /// Owned files no longer match the receipt. + public static SkillInstallationOwnershipState Modified { get; } = new("modified"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogTrustSource left, CatalogTrustSource right) => !(left == right); + /// Ownership evidence requires recovery before mutation. + public static SkillInstallationOwnershipState RecoveryRequired { get; } = new("recovery-required"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillInstallationOwnershipState left, SkillInstallationOwnershipState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInstallationOwnershipState left, SkillInstallationOwnershipState right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogTrustSource other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationOwnershipState other && Equals(other); /// - public bool Equals(CatalogTrustSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SkillInstallationOwnershipState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28432,59 +30783,68 @@ public CatalogTrustSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogTrustSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SkillInstallationOwnershipState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogTrustSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SkillInstallationOwnershipState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationOwnershipState)); } } } -/// Schema version of the catalogue trust snapshot envelope. +/// Bound-session observation after reconciling persisted enablement. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustSnapshotSchemaVersion : IEquatable +public readonly struct SkillInstallationSessionState : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogTrustSnapshotSchemaVersion(string value) + public SkillInstallationSessionState(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Initial envelope carrying one bounded service tier or one explicit unavailable state. - public static CatalogTrustSnapshotSchemaVersion V1 { get; } = new("v1"); + /// The selected session has loaded this Skill and it is enabled. + public static SkillInstallationSessionState LoadedEnabled { get; } = new("loaded-enabled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion right) => left.Equals(right); + /// The selected session has loaded this Skill or settings and it is disabled. + public static SkillInstallationSessionState LoadedDisabled { get; } = new("loaded-disabled"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion right) => !(left == right); + /// The selected session has not loaded Skills after the latest change. + public static SkillInstallationSessionState NotLoaded { get; } = new("not-loaded"); + + /// The selected session could not be inspected. + public static SkillInstallationSessionState Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillInstallationSessionState left, SkillInstallationSessionState right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInstallationSessionState left, SkillInstallationSessionState right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogTrustSnapshotSchemaVersion other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationSessionState other && Equals(other); /// - public bool Equals(CatalogTrustSnapshotSchemaVersion other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SkillInstallationSessionState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28492,62 +30852,146 @@ public CatalogTrustSnapshotSchemaVersion(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogTrustSnapshotSchemaVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SkillInstallationSessionState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogTrustSnapshotSchemaVersion value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SkillInstallationSessionState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSnapshotSchemaVersion)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationSessionState)); } } } -/// Service-computed trust tier currently emitted by Agent Finder. It is independent of search score, popularity, and client-side ranking. +/// Bounded refusal categories for verified Skill installation management. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustTier : IEquatable +public readonly struct SkillInstallationFailureReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogTrustTier(string value) + public SkillInstallationFailureReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Tier one as assigned by the catalogue authority. - public static CatalogTrustTier T1 { get; } = new("T1"); + /// The selected session does not have the Agent Finder Skill installation feature flag enabled for acquisition. + public static SkillInstallationFailureReason FeatureDisabled { get; } = new("feature-disabled"); - /// Tier two as assigned by the catalogue authority. - public static CatalogTrustTier T2 { get; } = new("T2"); + /// The request is unsupported or malformed. + public static SkillInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogTrustTier left, CatalogTrustTier right) => left.Equals(right); + /// The bounded operation limit was reached. + public static SkillInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogTrustTier left, CatalogTrustTier right) => !(left == right); + /// The original operation was cancelled. + public static SkillInstallationFailureReason Cancelled { get; } = new("cancelled"); + + /// The original host cannot receive human confirmation. + public static SkillInstallationFailureReason ConfirmationUnavailable { get; } = new("confirmation-unavailable"); + + /// The confirmation response is malformed or mismatched. + public static SkillInstallationFailureReason ConfirmationInvalid { get; } = new("confirmation-invalid"); + + /// Existing authenticated session and host authority is unavailable. + public static SkillInstallationFailureReason PolicyContextUnavailable { get; } = new("policy-context-unavailable"); + + /// The original authority or policy changed. + public static SkillInstallationFailureReason PolicyChanged { get; } = new("policy-changed"); + + /// No matching owned resource or original operation exists. + public static SkillInstallationFailureReason ResourceNotFound { get; } = new("resource-not-found"); + + /// The original plan deadline elapsed. + public static SkillInstallationFailureReason PlanExpired { get; } = new("plan-expired"); + + /// The one-use plan was already consumed. + public static SkillInstallationFailureReason PlanReplayed { get; } = new("plan-replayed"); + + /// The handle belongs to a different runtime, session or connection. + public static SkillInstallationFailureReason ForeignRuntime { get; } = new("foreign-runtime"); + + /// The handle is not the expected Skill handle kind. + public static SkillInstallationFailureReason WrongKind { get; } = new("wrong-kind"); + + /// The handle was minted for a different search result or authority. + public static SkillInstallationFailureReason SearchMismatch { get; } = new("search-mismatch"); + + /// The handle or operation expired. + public static SkillInstallationFailureReason Expired { get; } = new("expired"); + + /// The candidate handle is not a verified installable Skill candidate. + public static SkillInstallationFailureReason InvalidCandidate { get; } = new("invalid-candidate"); + + /// The verified Skill descriptor could not be retrieved safely. + public static SkillInstallationFailureReason DescriptorUnavailable { get; } = new("descriptor-unavailable"); + + /// The verified Skill descriptor failed validation. + public static SkillInstallationFailureReason DescriptorInvalid { get; } = new("descriptor-invalid"); + + /// The Skill entrypoint could not be retrieved or verified. + public static SkillInstallationFailureReason EntrypointUnavailable { get; } = new("entrypoint-unavailable"); + + /// The Skill entrypoint is not a valid Skill. + public static SkillInstallationFailureReason InvalidSkill { get; } = new("invalid-skill"); + + /// The reviewed Skill payload could not be acquired. + public static SkillInstallationFailureReason PayloadUnavailable { get; } = new("payload-unavailable"); + + /// The acquired payload no longer matches the reviewed descriptor. + public static SkillInstallationFailureReason PayloadMismatch { get; } = new("payload-mismatch"); + + /// The retained Skill source changed after planning. + public static SkillInstallationFailureReason SourceChanged { get; } = new("source-changed"); + + /// The selected runtime cannot inspect or control the requested lifecycle operation. + public static SkillInstallationFailureReason LifecycleUnavailable { get; } = new("lifecycle-unavailable"); + + /// Durable Skill installation evidence requires recovery before mutation. + public static SkillInstallationFailureReason RecoveryRequired { get; } = new("recovery-required"); + + /// The Skill entrypoint exceeds the bounded complete review size. + public static SkillInstallationFailureReason ReviewTooLarge { get; } = new("review-too-large"); + + /// Skill installation storage or admission is busy. + public static SkillInstallationFailureReason Busy { get; } = new("busy"); + + /// An owned Skill with the same identity or target already exists. + public static SkillInstallationFailureReason AlreadyInstalled { get; } = new("already-installed"); + + /// Installed Skill files no longer match ownership evidence. + public static SkillInstallationFailureReason ConfigurationModified { get; } = new("configuration-modified"); + + /// A storage operation failed; inspect durable state before retrying. + public static SkillInstallationFailureReason WriteFailed { get; } = new("write-failed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillInstallationFailureReason left, SkillInstallationFailureReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInstallationFailureReason left, SkillInstallationFailureReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogTrustTier other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationFailureReason other && Equals(other); /// - public bool Equals(CatalogTrustTier other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SkillInstallationFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28555,74 +30999,68 @@ public CatalogTrustTier(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogTrustTier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SkillInstallationFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogTrustTier value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SkillInstallationFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustTier)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationFailureReason)); } } } -/// Configuration ownership and setup observations, distinct from tool permissions. +/// Which tier this directory belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpInstallationState : IEquatable +public readonly struct SkillDiscoveryScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpInstallationState(string value) + public SkillDiscoveryScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Owned configuration exists; inventory alone does not grant activation. - public static McpInstallationState NeedsSetup { get; } = new("needs-setup"); - - /// The selected authorised session reports an active installation. - public static McpInstallationState Active { get; } = new("active"); - - /// The selected server requires explicit sign-in. - public static McpInstallationState AuthenticationRequired { get; } = new("authentication-required"); + /// A project's repository skill directory. + public static SkillDiscoveryScope Project { get; } = new("project"); - /// The selected server could not be activated. - public static McpInstallationState ActivationFailed { get; } = new("activation-failed"); + /// The user's personal Copilot skill directory. + public static SkillDiscoveryScope PersonalCopilot { get; } = new("personal-copilot"); - /// Owned configuration no longer matches its receipt. - public static McpInstallationState ConfigurationModified { get; } = new("configuration-modified"); + /// The user's personal agents skill directory. + public static SkillDiscoveryScope PersonalAgents { get; } = new("personal-agents"); - /// Confirmed durable work or unsafe evidence requires recovery. - public static McpInstallationState RecoveryRequired { get; } = new("recovery-required"); + /// A configured custom skill directory. + public static SkillDiscoveryScope Custom { get; } = new("custom"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpInstallationState left, McpInstallationState right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillDiscoveryScope left, SkillDiscoveryScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpInstallationState left, McpInstallationState right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillDiscoveryScope left, SkillDiscoveryScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpInstallationState other && Equals(other); + public override bool Equals(object? obj) => obj is SkillDiscoveryScope other && Equals(other); /// - public bool Equals(McpInstallationState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SkillDiscoveryScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28630,125 +31068,62 @@ public McpInstallationState(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpInstallationState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SkillDiscoveryScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpInstallationState value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SkillDiscoveryScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillDiscoveryScope)); } } } -/// Bounded refusal categories, without echoing handles, credentials or configuration. +/// Whether an MCP server candidate can be planned for installation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpInstallationFailureReason : IEquatable +public readonly struct CatalogMcpServerInstallability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpInstallationFailureReason(string value) + public CatalogMcpServerInstallability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The selected choice or request is unsupported or malformed. - public static McpInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - - /// The bounded original-connection operation limit was reached. - public static McpInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - - /// The original operation was cancelled. - public static McpInstallationFailureReason Cancelled { get; } = new("cancelled"); - - /// Required installation capabilities were omitted. - public static McpInstallationFailureReason CapabilityRequired { get; } = new("capability-required"); - - /// The original host cannot receive human confirmation. - public static McpInstallationFailureReason ConfirmationUnavailable { get; } = new("confirmation-unavailable"); - - /// The confirmation response is malformed or mismatched. - public static McpInstallationFailureReason ConfirmationInvalid { get; } = new("confirmation-invalid"); - - /// Existing authenticated session and host authority is unavailable. - public static McpInstallationFailureReason PolicyContextUnavailable { get; } = new("policy-context-unavailable"); - - /// The original authority or policy changed. - public static McpInstallationFailureReason PolicyChanged { get; } = new("policy-changed"); - - /// Current managed policy refuses the operation. - public static McpInstallationFailureReason PolicyDenied { get; } = new("policy-denied"); - - /// Configuration changed after the reviewed snapshot. - public static McpInstallationFailureReason ConfigurationChanged { get; } = new("configuration-changed"); - - /// Installed configuration no longer matches ownership evidence. - public static McpInstallationFailureReason ConfigurationModified { get; } = new("configuration-modified"); - - /// No matching owned resource or original operation exists. - public static McpInstallationFailureReason ResourceNotFound { get; } = new("resource-not-found"); - - /// The original plan deadline elapsed. - public static McpInstallationFailureReason PlanExpired { get; } = new("plan-expired"); - - /// The one-use plan or prepared operation was already consumed. - public static McpInstallationFailureReason PlanReplayed { get; } = new("plan-replayed"); - - /// The handle belongs to a different runtime, session or connection. - public static McpInstallationFailureReason ForeignRuntime { get; } = new("foreign-runtime"); - - /// Fresh bound planning is required. - public static McpInstallationFailureReason ReplanRequired { get; } = new("replan-required"); - - /// Exact original source revalidation is unsupported. - public static McpInstallationFailureReason SourceRevalidationUnavailable { get; } = new("source-revalidation-unavailable"); - - /// The source differs from the retained commitment. - public static McpInstallationFailureReason SourceChanged { get; } = new("source-changed"); - - /// The original source could not be retrieved safely. - public static McpInstallationFailureReason SourceUnavailable { get; } = new("source-unavailable"); - - /// Authoritative Registry interpretation is unavailable. - public static McpInstallationFailureReason RegistryUnavailable { get; } = new("registry-unavailable"); - - /// The selected secret backend is unavailable. - public static McpInstallationFailureReason SecretStoreUnavailable { get; } = new("secret-store-unavailable"); - - /// Required owned admission and lifecycle support is absent. - public static McpInstallationFailureReason LifecycleUnavailable { get; } = new("lifecycle-unavailable"); + /// An install plan can be computed for this MCP server candidate. + public static CatalogMcpServerInstallability Installable { get; } = new("installable"); - /// A storage operation failed; inspect any allocated operation before retrying. - public static McpInstallationFailureReason WriteFailed { get; } = new("write-failed"); + /// Policy forbids installing this MCP server candidate. + public static CatalogMcpServerInstallability NotInstallablePolicy { get; } = new("not-installable-policy"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpInstallationFailureReason left, McpInstallationFailureReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogMcpServerInstallability left, CatalogMcpServerInstallability right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpInstallationFailureReason left, McpInstallationFailureReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogMcpServerInstallability left, CatalogMcpServerInstallability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpInstallationFailureReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMcpServerInstallability other && Equals(other); /// - public bool Equals(McpInstallationFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogMcpServerInstallability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28756,62 +31131,71 @@ public McpInstallationFailureReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpInstallationFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogMcpServerInstallability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpInstallationFailureReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogMcpServerInstallability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationFailureReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMcpServerInstallability)); } } } -/// Explicit backend selection is part of the final review; failures never switch backends. +/// Typed installability state for an AI skill candidate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpInstallationSecretStorage : IEquatable +public readonly struct CatalogAiSkillInstallability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpInstallationSecretStorage(string value) + public CatalogAiSkillInstallability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The selected operating-system keychain, without fallback to file storage. - public static McpInstallationSecretStorage Keychain { get; } = new("keychain"); + /// This AI skill candidate carries verified materialisation metadata and the selected session may plan installation. + public static CatalogAiSkillInstallability Installable { get; } = new("installable"); - /// The explicitly selected private file backend. - public static McpInstallationSecretStorage PrivateFile { get; } = new("private-file"); + /// Skill installation is understood but disabled for the selected session. + public static CatalogAiSkillInstallability FeatureDisabled { get; } = new("feature-disabled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpInstallationSecretStorage left, McpInstallationSecretStorage right) => left.Equals(right); + /// The candidate lacks verified materialisation metadata required for installation. + public static CatalogAiSkillInstallability MaterialisationUnavailable { get; } = new("materialisation-unavailable"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpInstallationSecretStorage left, McpInstallationSecretStorage right) => !(left == right); + /// Policy refuses Skill installation for the selected session or authority. + public static CatalogAiSkillInstallability PolicyForbids { get; } = new("policy-forbids"); + + /// Compatibility value for discovery-only callers that did not negotiate Skill installation. + public static CatalogAiSkillInstallability NotInstallableKind { get; } = new("not-installable-kind"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogAiSkillInstallability left, CatalogAiSkillInstallability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogAiSkillInstallability left, CatalogAiSkillInstallability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpInstallationSecretStorage other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAiSkillInstallability other && Equals(other); /// - public bool Equals(McpInstallationSecretStorage other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogAiSkillInstallability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28819,62 +31203,59 @@ public McpInstallationSecretStorage(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpInstallationSecretStorage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogAiSkillInstallability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpInstallationSecretStorage value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogAiSkillInstallability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationSecretStorage)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillInstallability)); } } } -/// Persisted extension discovery source. +/// Canonical AI skill media type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiscoveredExtensionSource : IEquatable +public readonly struct CatalogAiSkillMediaType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiscoveredExtensionSource(string value) + public CatalogAiSkillMediaType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Extension discovered from the user's extensions directory. - public static DiscoveredExtensionSource User { get; } = new("user"); - - /// Extension contributed by an installed plugin. - public static DiscoveredExtensionSource Plugin { get; } = new("plugin"); + /// An AI skill card. + public static CatalogAiSkillMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiscoveredExtensionSource left, DiscoveredExtensionSource right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiscoveredExtensionSource left, DiscoveredExtensionSource right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiscoveredExtensionSource other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAiSkillMediaType other && Equals(other); /// - public bool Equals(DiscoveredExtensionSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogAiSkillMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28882,65 +31263,65 @@ public DiscoveredExtensionSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiscoveredExtensionSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogAiSkillMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiscoveredExtensionSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogAiSkillMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillMediaType)); } } } -/// Effective extension loading and agent-management mode. +/// Explicit Agent Plugin compatibility declared by exact catalog tags. Clients must not infer these values from display text or other metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiscoveredExtensionMode : IEquatable +public readonly struct CatalogAgentPluginCompatibilityTag : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiscoveredExtensionMode(string value) + public CatalogAgentPluginCompatibilityTag(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Extensions are not loaded. - public static DiscoveredExtensionMode Disabled { get; } = new("disabled"); + /// The plugin contributes at least one GitHub Copilot Canvas. + public static CatalogAgentPluginCompatibilityTag Canvas { get; } = new("canvas"); - /// Extensions are loaded, but the agent cannot create, reload, or manage them. - public static DiscoveredExtensionMode LoadOnly { get; } = new("load_only"); + /// The plugin depends on Canvas for its intended functionality. + public static CatalogAgentPluginCompatibilityTag CanvasOnly { get; } = new("canvas-only"); - /// Extensions are loaded and the agent can create, reload, and manage them. - public static DiscoveredExtensionMode LoadAndAugment { get; } = new("load_and_augment"); + /// The plugin targets GitHub Copilot. + public static CatalogAgentPluginCompatibilityTag GitHubCopilot { get; } = new("github-copilot"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiscoveredExtensionMode left, DiscoveredExtensionMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiscoveredExtensionMode left, DiscoveredExtensionMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiscoveredExtensionMode other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAgentPluginCompatibilityTag other && Equals(other); /// - public bool Equals(DiscoveredExtensionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogAgentPluginCompatibilityTag other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28948,59 +31329,59 @@ public DiscoveredExtensionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiscoveredExtensionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogAgentPluginCompatibilityTag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiscoveredExtensionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogAgentPluginCompatibilityTag value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginCompatibilityTag)); } } } -/// Defines the allowed values. +/// Canonical Agent Plugin media type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillInstallationScope : IEquatable +public readonly struct CatalogAgentPluginMediaType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SkillInstallationScope(string value) + public CatalogAgentPluginMediaType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The user's personal Copilot home. - public static SkillInstallationScope Personal { get; } = new("personal"); + /// A GitHub Copilot Agent Plugin descriptor. + public static CatalogAgentPluginMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SkillInstallationScope left, SkillInstallationScope right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SkillInstallationScope left, SkillInstallationScope right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SkillInstallationScope other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAgentPluginMediaType other && Equals(other); /// - public bool Equals(SkillInstallationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogAgentPluginMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29008,65 +31389,59 @@ public SkillInstallationScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SkillInstallationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogAgentPluginMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SkillInstallationScope value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogAgentPluginMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginMediaType)); } } } -/// Owned Skill state observed from files and receipts. +/// Relationship of the backend-reported count to the complete query result set. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillInstallationOwnershipState : IEquatable +public readonly struct CatalogSearchTotalCountRelation : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SkillInstallationOwnershipState(string value) + public CatalogSearchTotalCountRelation(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Owned files and receipt evidence match. - public static SkillInstallationOwnershipState Intact { get; } = new("intact"); - - /// Owned files no longer match the receipt. - public static SkillInstallationOwnershipState Modified { get; } = new("modified"); - - /// Ownership evidence requires recovery before mutation. - public static SkillInstallationOwnershipState RecoveryRequired { get; } = new("recovery-required"); + /// No exact/full-query or lower-bound guarantee is available. + public static CatalogSearchTotalCountRelation Unknown { get; } = new("unknown"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SkillInstallationOwnershipState left, SkillInstallationOwnershipState right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogSearchTotalCountRelation left, CatalogSearchTotalCountRelation right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SkillInstallationOwnershipState left, SkillInstallationOwnershipState right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogSearchTotalCountRelation left, CatalogSearchTotalCountRelation right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SkillInstallationOwnershipState other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogSearchTotalCountRelation other && Equals(other); /// - public bool Equals(SkillInstallationOwnershipState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogSearchTotalCountRelation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29074,68 +31449,65 @@ public SkillInstallationOwnershipState(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SkillInstallationOwnershipState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogSearchTotalCountRelation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SkillInstallationOwnershipState value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogSearchTotalCountRelation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationOwnershipState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSearchTotalCountRelation)); } } } -/// Bound-session observation after reconciling persisted enablement. +/// What kind of resource a catalog candidate describes. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillInstallationSessionState : IEquatable +public readonly struct CatalogCandidateKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SkillInstallationSessionState(string value) + public CatalogCandidateKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The selected session has loaded this Skill and it is enabled. - public static SkillInstallationSessionState LoadedEnabled { get; } = new("loaded-enabled"); - - /// The selected session has loaded this Skill or settings and it is disabled. - public static SkillInstallationSessionState LoadedDisabled { get; } = new("loaded-disabled"); + /// An MCP server, which can be planned for installation. + public static CatalogCandidateKind McpServer { get; } = new("mcp-server"); - /// The selected session has not loaded Skills after the latest change. - public static SkillInstallationSessionState NotLoaded { get; } = new("not-loaded"); + /// An AI skill, which is discoverable but not installable through this surface. + public static CatalogCandidateKind AiSkill { get; } = new("ai-skill"); - /// The selected session could not be inspected. - public static SkillInstallationSessionState Unknown { get; } = new("unknown"); + /// An inert Agent Plugin candidate, available only when explicitly requested. + public static CatalogCandidateKind Plugin { get; } = new("plugin"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SkillInstallationSessionState left, SkillInstallationSessionState right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogCandidateKind left, CatalogCandidateKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SkillInstallationSessionState left, SkillInstallationSessionState right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogCandidateKind left, CatalogCandidateKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SkillInstallationSessionState other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogCandidateKind other && Equals(other); /// - public bool Equals(SkillInstallationSessionState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogCandidateKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29143,146 +31515,131 @@ public SkillInstallationSessionState(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SkillInstallationSessionState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CatalogCandidateKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SkillInstallationSessionState value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CatalogCandidateKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationSessionState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCandidateKind)); } } } -/// Bounded refusal categories for verified Skill installation management. +/// Terminal outcome declared for a retained catalog selection group. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillInstallationFailureReason : IEquatable +public readonly struct CatalogSelectionDecision : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SkillInstallationFailureReason(string value) + public CatalogSelectionDecision(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The selected session does not have the Agent Finder Skill installation feature flag enabled for acquisition. - public static SkillInstallationFailureReason FeatureDisabled { get; } = new("feature-disabled"); - - /// The request is unsupported or malformed. - public static SkillInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - - /// The bounded operation limit was reached. - public static SkillInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - - /// The original operation was cancelled. - public static SkillInstallationFailureReason Cancelled { get; } = new("cancelled"); - - /// The original host cannot receive human confirmation. - public static SkillInstallationFailureReason ConfirmationUnavailable { get; } = new("confirmation-unavailable"); - - /// The confirmation response is malformed or mismatched. - public static SkillInstallationFailureReason ConfirmationInvalid { get; } = new("confirmation-invalid"); - - /// Existing authenticated session and host authority is unavailable. - public static SkillInstallationFailureReason PolicyContextUnavailable { get; } = new("policy-context-unavailable"); - - /// The original authority or policy changed. - public static SkillInstallationFailureReason PolicyChanged { get; } = new("policy-changed"); - - /// No matching owned resource or original operation exists. - public static SkillInstallationFailureReason ResourceNotFound { get; } = new("resource-not-found"); - - /// The original plan deadline elapsed. - public static SkillInstallationFailureReason PlanExpired { get; } = new("plan-expired"); - - /// The one-use plan was already consumed. - public static SkillInstallationFailureReason PlanReplayed { get; } = new("plan-replayed"); - - /// The handle belongs to a different runtime, session or connection. - public static SkillInstallationFailureReason ForeignRuntime { get; } = new("foreign-runtime"); - - /// The handle is not the expected Skill handle kind. - public static SkillInstallationFailureReason WrongKind { get; } = new("wrong-kind"); + /// Choose the candidate named by selectionRef. + public static CatalogSelectionDecision Selected { get; } = new("selected"); - /// The handle was minted for a different search result or authority. - public static SkillInstallationFailureReason SearchMismatch { get; } = new("search-mismatch"); + /// Explicitly decline every candidate in the search. + public static CatalogSelectionDecision Declined { get; } = new("declined"); - /// The handle or operation expired. - public static SkillInstallationFailureReason Expired { get; } = new("expired"); + /// Cancel the selection interaction without choosing a candidate. + public static CatalogSelectionDecision Cancelled { get; } = new("cancelled"); - /// The candidate handle is not a verified installable Skill candidate. - public static SkillInstallationFailureReason InvalidCandidate { get; } = new("invalid-candidate"); + /// Declare that the host's live interaction deadline elapsed while the reference remained valid. + public static CatalogSelectionDecision TimedOut { get; } = new("timed-out"); - /// The verified Skill descriptor could not be retrieved safely. - public static SkillInstallationFailureReason DescriptorUnavailable { get; } = new("descriptor-unavailable"); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogSelectionDecision left, CatalogSelectionDecision right) => left.Equals(right); - /// The verified Skill descriptor failed validation. - public static SkillInstallationFailureReason DescriptorInvalid { get; } = new("descriptor-invalid"); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogSelectionDecision left, CatalogSelectionDecision right) => !(left == right); - /// The Skill entrypoint could not be retrieved or verified. - public static SkillInstallationFailureReason EntrypointUnavailable { get; } = new("entrypoint-unavailable"); + /// + public override bool Equals(object? obj) => obj is CatalogSelectionDecision other && Equals(other); - /// The Skill entrypoint is not a valid Skill. - public static SkillInstallationFailureReason InvalidSkill { get; } = new("invalid-skill"); + /// + public bool Equals(CatalogSelectionDecision other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// The reviewed Skill payload could not be acquired. - public static SkillInstallationFailureReason PayloadUnavailable { get; } = new("payload-unavailable"); + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// The acquired payload no longer matches the reviewed descriptor. - public static SkillInstallationFailureReason PayloadMismatch { get; } = new("payload-mismatch"); + /// + public override string ToString() => Value; - /// The retained Skill source changed after planning. - public static SkillInstallationFailureReason SourceChanged { get; } = new("source-changed"); + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CatalogSelectionDecision Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// The selected runtime cannot inspect or control the requested lifecycle operation. - public static SkillInstallationFailureReason LifecycleUnavailable { get; } = new("lifecycle-unavailable"); + /// + public override void Write(Utf8JsonWriter writer, CatalogSelectionDecision value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSelectionDecision)); + } + } +} - /// Durable Skill installation evidence requires recovery before mutation. - public static SkillInstallationFailureReason RecoveryRequired { get; } = new("recovery-required"); - /// The Skill entrypoint exceeds the bounded complete review size. - public static SkillInstallationFailureReason ReviewTooLarge { get; } = new("review-too-large"); +/// Where completed plugin content was staged before atomic promotion. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct PluginInstallStagingMode : IEquatable +{ + private readonly string? _value; - /// Skill installation storage or admission is busy. - public static SkillInstallationFailureReason Busy { get; } = new("busy"); + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public PluginInstallStagingMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// An owned Skill with the same identity or target already exists. - public static SkillInstallationFailureReason AlreadyInstalled { get; } = new("already-installed"); + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Installed Skill files no longer match ownership evidence. - public static SkillInstallationFailureReason ConfigurationModified { get; } = new("configuration-modified"); + /// A sibling of the installed-plugins root, outside the recursively watched tree. + public static PluginInstallStagingMode External { get; } = new("external"); - /// A storage operation failed; inspect durable state before retrying. - public static SkillInstallationFailureReason WriteFailed { get; } = new("write-failed"); + /// A sibling of the destination plugin directory, used when external staging is unavailable. + public static PluginInstallStagingMode DestinationSibling { get; } = new("destination_sibling"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SkillInstallationFailureReason left, SkillInstallationFailureReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PluginInstallStagingMode left, PluginInstallStagingMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SkillInstallationFailureReason left, SkillInstallationFailureReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PluginInstallStagingMode left, PluginInstallStagingMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SkillInstallationFailureReason other && Equals(other); + public override bool Equals(object? obj) => obj is PluginInstallStagingMode other && Equals(other); /// - public bool Equals(SkillInstallationFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PluginInstallStagingMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29290,68 +31647,74 @@ public SkillInstallationFailureReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SkillInstallationFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PluginInstallStagingMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SkillInstallationFailureReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PluginInstallStagingMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationFailureReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); } } } -/// Which tier this directory belongs to. +/// Where the agent definition was loaded from. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillDiscoveryScope : IEquatable +public readonly struct AgentInfoSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SkillDiscoveryScope(string value) + public AgentInfoSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A project's repository skill directory. - public static SkillDiscoveryScope Project { get; } = new("project"); + /// Agent loaded from the user's personal agent configuration. + public static AgentInfoSource User { get; } = new("user"); - /// The user's personal Copilot skill directory. - public static SkillDiscoveryScope PersonalCopilot { get; } = new("personal-copilot"); + /// Agent loaded from the current project's repository configuration. + public static AgentInfoSource Project { get; } = new("project"); - /// The user's personal agents skill directory. - public static SkillDiscoveryScope PersonalAgents { get; } = new("personal-agents"); + /// Agent inherited from a parent project or workspace. + public static AgentInfoSource Inherited { get; } = new("inherited"); - /// A configured custom skill directory. - public static SkillDiscoveryScope Custom { get; } = new("custom"); + /// Agent provided by a remote runtime or service. + public static AgentInfoSource Remote { get; } = new("remote"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SkillDiscoveryScope left, SkillDiscoveryScope right) => left.Equals(right); + /// Agent contributed by an installed plugin. + public static AgentInfoSource Plugin { get; } = new("plugin"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SkillDiscoveryScope left, SkillDiscoveryScope right) => !(left == right); + /// Agent built into the Copilot runtime. + public static AgentInfoSource Builtin { get; } = new("builtin"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentInfoSource left, AgentInfoSource right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentInfoSource left, AgentInfoSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SkillDiscoveryScope other && Equals(other); + public override bool Equals(object? obj) => obj is AgentInfoSource other && Equals(other); /// - public bool Equals(SkillDiscoveryScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentInfoSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29359,62 +31722,62 @@ public SkillDiscoveryScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SkillDiscoveryScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentInfoSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SkillDiscoveryScope value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentInfoSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillDiscoveryScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentInfoSource)); } } } -/// Whether an MCP server candidate can be planned for installation. +/// Which tier this directory belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogMcpServerInstallability : IEquatable +public readonly struct AgentDiscoveryPathScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogMcpServerInstallability(string value) + public AgentDiscoveryPathScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// An install plan can be computed for this MCP server candidate. - public static CatalogMcpServerInstallability Installable { get; } = new("installable"); + /// The user's personal agent configuration directory. + public static AgentDiscoveryPathScope User { get; } = new("user"); - /// Policy forbids installing this MCP server candidate. - public static CatalogMcpServerInstallability NotInstallablePolicy { get; } = new("not-installable-policy"); + /// A project's repository agent directory. + public static AgentDiscoveryPathScope Project { get; } = new("project"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogMcpServerInstallability left, CatalogMcpServerInstallability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentDiscoveryPathScope left, AgentDiscoveryPathScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogMcpServerInstallability left, CatalogMcpServerInstallability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentDiscoveryPathScope left, AgentDiscoveryPathScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogMcpServerInstallability other && Equals(other); + public override bool Equals(object? obj) => obj is AgentDiscoveryPathScope other && Equals(other); /// - public bool Equals(CatalogMcpServerInstallability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentDiscoveryPathScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29422,71 +31785,68 @@ public CatalogMcpServerInstallability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogMcpServerInstallability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentDiscoveryPathScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogMcpServerInstallability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentDiscoveryPathScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMcpServerInstallability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentDiscoveryPathScope)); } } } -/// Typed installability state for an AI skill candidate. +/// Where this source lives — used for UI grouping. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAiSkillInstallability : IEquatable +public readonly struct InstructionSourceLocation : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogAiSkillInstallability(string value) + public InstructionSourceLocation(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// This AI skill candidate carries verified materialisation metadata and the selected session may plan installation. - public static CatalogAiSkillInstallability Installable { get; } = new("installable"); - - /// Skill installation is understood but disabled for the selected session. - public static CatalogAiSkillInstallability FeatureDisabled { get; } = new("feature-disabled"); + /// Instructions live in user-level configuration. + public static InstructionSourceLocation User { get; } = new("user"); - /// The candidate lacks verified materialisation metadata required for installation. - public static CatalogAiSkillInstallability MaterialisationUnavailable { get; } = new("materialisation-unavailable"); + /// Instructions live in repository-level configuration. + public static InstructionSourceLocation Repository { get; } = new("repository"); - /// Policy refuses Skill installation for the selected session or authority. - public static CatalogAiSkillInstallability PolicyForbids { get; } = new("policy-forbids"); + /// Instructions live under the current working directory. + public static InstructionSourceLocation WorkingDirectory { get; } = new("working-directory"); - /// Compatibility value for discovery-only callers that did not negotiate Skill installation. - public static CatalogAiSkillInstallability NotInstallableKind { get; } = new("not-installable-kind"); + /// Instructions live in plugin-provided configuration. + public static InstructionSourceLocation Plugin { get; } = new("plugin"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAiSkillInstallability left, CatalogAiSkillInstallability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(InstructionSourceLocation left, InstructionSourceLocation right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAiSkillInstallability left, CatalogAiSkillInstallability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(InstructionSourceLocation left, InstructionSourceLocation right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAiSkillInstallability other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionSourceLocation other && Equals(other); /// - public bool Equals(CatalogAiSkillInstallability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(InstructionSourceLocation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29494,59 +31854,77 @@ public CatalogAiSkillInstallability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogAiSkillInstallability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override InstructionSourceLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogAiSkillInstallability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, InstructionSourceLocation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillInstallability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceLocation)); } } } -/// Canonical AI skill media type. +/// Category of instruction source — used for merge logic. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAiSkillMediaType : IEquatable +public readonly struct InstructionSourceType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogAiSkillMediaType(string value) + public InstructionSourceType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// An AI skill card. - public static CatalogAiSkillMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); + /// Instructions loaded from the user's home configuration. + public static InstructionSourceType Home { get; } = new("home"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => left.Equals(right); + /// Instructions loaded from repository-scoped files. + public static InstructionSourceType Repo { get; } = new("repo"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => !(left == right); + /// Instructions loaded from model-specific files. + public static InstructionSourceType Model { get; } = new("model"); + + /// Instructions loaded from VS Code instruction files. + public static InstructionSourceType Vscode { get; } = new("vscode"); + + /// Instructions discovered from nested agent files. + public static InstructionSourceType NestedAgents { get; } = new("nested-agents"); + + /// Instructions inherited from child instruction files. + public static InstructionSourceType ChildInstructions { get; } = new("child-instructions"); + + /// Instructions supplied by an installed plugin. + public static InstructionSourceType Plugin { get; } = new("plugin"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(InstructionSourceType left, InstructionSourceType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(InstructionSourceType left, InstructionSourceType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAiSkillMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionSourceType other && Equals(other); /// - public bool Equals(CatalogAiSkillMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(InstructionSourceType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29554,65 +31932,62 @@ public CatalogAiSkillMediaType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogAiSkillMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override InstructionSourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogAiSkillMediaType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, InstructionSourceType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceType)); } } } -/// Explicit Agent Plugin compatibility declared by exact catalog tags. Clients must not infer these values from display text or other metadata. +/// Whether the target is a single file or a directory of instruction files. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAgentPluginCompatibilityTag : IEquatable +public readonly struct InstructionDiscoveryPathKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogAgentPluginCompatibilityTag(string value) + public InstructionDiscoveryPathKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The plugin contributes at least one GitHub Copilot Canvas. - public static CatalogAgentPluginCompatibilityTag Canvas { get; } = new("canvas"); - - /// The plugin depends on Canvas for its intended functionality. - public static CatalogAgentPluginCompatibilityTag CanvasOnly { get; } = new("canvas-only"); + /// The target is a single instruction file. + public static InstructionDiscoveryPathKind File { get; } = new("file"); - /// The plugin targets GitHub Copilot. - public static CatalogAgentPluginCompatibilityTag GitHubCopilot { get; } = new("github-copilot"); + /// The target is a directory that holds instruction files. + public static InstructionDiscoveryPathKind Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(InstructionDiscoveryPathKind left, InstructionDiscoveryPathKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(InstructionDiscoveryPathKind left, InstructionDiscoveryPathKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAgentPluginCompatibilityTag other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionDiscoveryPathKind other && Equals(other); /// - public bool Equals(CatalogAgentPluginCompatibilityTag other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(InstructionDiscoveryPathKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29620,59 +31995,68 @@ public CatalogAgentPluginCompatibilityTag(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogAgentPluginCompatibilityTag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override InstructionDiscoveryPathKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogAgentPluginCompatibilityTag value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, InstructionDiscoveryPathKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginCompatibilityTag)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathKind)); } } } -/// Canonical Agent Plugin media type. +/// Which tier this target belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAgentPluginMediaType : IEquatable +public readonly struct InstructionDiscoveryPathLocation : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogAgentPluginMediaType(string value) + public InstructionDiscoveryPathLocation(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A GitHub Copilot Agent Plugin descriptor. - public static CatalogAgentPluginMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); + /// Instructions live in user-level configuration. + public static InstructionDiscoveryPathLocation User { get; } = new("user"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => left.Equals(right); + /// Instructions live in repository-level configuration. + public static InstructionDiscoveryPathLocation Repository { get; } = new("repository"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => !(left == right); + /// Instructions live under the current working directory. + public static InstructionDiscoveryPathLocation WorkingDirectory { get; } = new("working-directory"); + + /// Instructions live in plugin-provided configuration. + public static InstructionDiscoveryPathLocation Plugin { get; } = new("plugin"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAgentPluginMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionDiscoveryPathLocation other && Equals(other); /// - public bool Equals(CatalogAgentPluginMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(InstructionDiscoveryPathLocation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29680,59 +32064,59 @@ public CatalogAgentPluginMediaType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogAgentPluginMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override InstructionDiscoveryPathLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogAgentPluginMediaType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, InstructionDiscoveryPathLocation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathLocation)); } } } -/// Relationship of the backend-reported count to the complete query result set. +/// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogSearchTotalCountRelation : IEquatable +public readonly struct SlashCommandInputCompletion : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogSearchTotalCountRelation(string value) + public SlashCommandInputCompletion(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No exact/full-query or lower-bound guarantee is available. - public static CatalogSearchTotalCountRelation Unknown { get; } = new("unknown"); + /// Input should complete filesystem directories. + public static SlashCommandInputCompletion Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogSearchTotalCountRelation left, CatalogSearchTotalCountRelation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogSearchTotalCountRelation left, CatalogSearchTotalCountRelation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogSearchTotalCountRelation other && Equals(other); + public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); /// - public bool Equals(CatalogSearchTotalCountRelation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SlashCommandInputCompletion other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29740,65 +32124,65 @@ public CatalogSearchTotalCountRelation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogSearchTotalCountRelation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SlashCommandInputCompletion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogSearchTotalCountRelation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SlashCommandInputCompletion value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSearchTotalCountRelation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); } } } -/// What kind of resource a catalog candidate describes. +/// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogCandidateKind : IEquatable +public readonly struct SlashCommandKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogCandidateKind(string value) + public SlashCommandKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// An MCP server, which can be planned for installation. - public static CatalogCandidateKind McpServer { get; } = new("mcp-server"); + /// Command implemented by the runtime. + public static SlashCommandKind Builtin { get; } = new("builtin"); - /// An AI skill, which is discoverable but not installable through this surface. - public static CatalogCandidateKind AiSkill { get; } = new("ai-skill"); + /// Command backed by a skill. + public static SlashCommandKind Skill { get; } = new("skill"); - /// An inert Agent Plugin candidate, available only when explicitly requested. - public static CatalogCandidateKind Plugin { get; } = new("plugin"); + /// Command registered by an SDK client or extension. + public static SlashCommandKind Client { get; } = new("client"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogCandidateKind left, CatalogCandidateKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SlashCommandKind left, SlashCommandKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogCandidateKind left, CatalogCandidateKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SlashCommandKind left, SlashCommandKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogCandidateKind other && Equals(other); + public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); /// - public bool Equals(CatalogCandidateKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SlashCommandKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29806,68 +32190,62 @@ public CatalogCandidateKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogCandidateKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SlashCommandKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogCandidateKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SlashCommandKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCandidateKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); } } } -/// Terminal outcome declared for a retained catalog selection group. +/// Hosting platform type of the repository. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogSelectionDecision : IEquatable +public readonly struct SessionWorkingDirectoryContextHostType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public CatalogSelectionDecision(string value) + public SessionWorkingDirectoryContextHostType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Choose the candidate named by selectionRef. - public static CatalogSelectionDecision Selected { get; } = new("selected"); - - /// Explicitly decline every candidate in the search. - public static CatalogSelectionDecision Declined { get; } = new("declined"); - - /// Cancel the selection interaction without choosing a candidate. - public static CatalogSelectionDecision Cancelled { get; } = new("cancelled"); + /// The working directory repository is hosted on GitHub. + public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github"); - /// Declare that the host's live interaction deadline elapsed while the reference remained valid. - public static CatalogSelectionDecision TimedOut { get; } = new("timed-out"); + /// The working directory repository is hosted on Azure DevOps. + public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogSelectionDecision left, CatalogSelectionDecision right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogSelectionDecision left, CatalogSelectionDecision right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogSelectionDecision other && Equals(other); + public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other); /// - public bool Equals(CatalogSelectionDecision other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionWorkingDirectoryContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29875,62 +32253,62 @@ public CatalogSelectionDecision(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override CatalogSelectionDecision Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionWorkingDirectoryContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, CatalogSelectionDecision value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSelectionDecision)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType)); } } } -/// Where completed plugin content was staged before atomic promotion. +/// Severity of a managed-settings validation finding. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PluginInstallStagingMode : IEquatable +public readonly struct ManagedSettingsDiagnosticSeverity : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PluginInstallStagingMode(string value) + public ManagedSettingsDiagnosticSeverity(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A sibling of the installed-plugins root, outside the recursively watched tree. - public static PluginInstallStagingMode External { get; } = new("external"); + /// The runtime rejects the document. + public static ManagedSettingsDiagnosticSeverity Error { get; } = new("error"); - /// A sibling of the destination plugin directory, used when external staging is unavailable. - public static PluginInstallStagingMode DestinationSibling { get; } = new("destination_sibling"); + /// The runtime accepts the document but ignores the flagged content. + public static ManagedSettingsDiagnosticSeverity Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PluginInstallStagingMode left, PluginInstallStagingMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ManagedSettingsDiagnosticSeverity left, ManagedSettingsDiagnosticSeverity right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PluginInstallStagingMode left, PluginInstallStagingMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ManagedSettingsDiagnosticSeverity left, ManagedSettingsDiagnosticSeverity right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PluginInstallStagingMode other && Equals(other); + public override bool Equals(object? obj) => obj is ManagedSettingsDiagnosticSeverity other && Equals(other); /// - public bool Equals(PluginInstallStagingMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ManagedSettingsDiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -29938,74 +32316,65 @@ public PluginInstallStagingMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PluginInstallStagingMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ManagedSettingsDiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PluginInstallStagingMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ManagedSettingsDiagnosticSeverity value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsDiagnosticSeverity)); } } } -/// Where the agent definition was loaded from. +/// A channel accepted by managedSettings.compose. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentInfoSource : IEquatable +public readonly struct ManagedSettingsChannel : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentInfoSource(string value) + public ManagedSettingsChannel(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Agent loaded from the user's personal agent configuration. - public static AgentInfoSource User { get; } = new("user"); - - /// Agent loaded from the current project's repository configuration. - public static AgentInfoSource Project { get; } = new("project"); - - /// Agent inherited from a parent project or workspace. - public static AgentInfoSource Inherited { get; } = new("inherited"); - - /// Agent provided by a remote runtime or service. - public static AgentInfoSource Remote { get; } = new("remote"); + /// Device policy, the strongest channel. + public static ManagedSettingsChannel Device { get; } = new("device"); - /// Agent contributed by an installed plugin. - public static AgentInfoSource Plugin { get; } = new("plugin"); + /// Account or organization policy. + public static ManagedSettingsChannel Server { get; } = new("server"); - /// Agent built into the Copilot runtime. - public static AgentInfoSource Builtin { get; } = new("builtin"); + /// Session-local helper output, the weakest channel. + public static ManagedSettingsChannel PolicyHelper { get; } = new("policyHelper"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentInfoSource left, AgentInfoSource right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ManagedSettingsChannel left, ManagedSettingsChannel right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentInfoSource left, AgentInfoSource right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ManagedSettingsChannel left, ManagedSettingsChannel right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentInfoSource other && Equals(other); + public override bool Equals(object? obj) => obj is ManagedSettingsChannel other && Equals(other); /// - public bool Equals(AgentInfoSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ManagedSettingsChannel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30013,62 +32382,62 @@ public AgentInfoSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentInfoSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ManagedSettingsChannel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentInfoSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ManagedSettingsChannel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentInfoSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsChannel)); } } } -/// Which tier this directory belongs to. +/// Path conventions used by this filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentDiscoveryPathScope : IEquatable +public readonly struct SessionFsSetProviderConventions : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentDiscoveryPathScope(string value) + public SessionFsSetProviderConventions(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The user's personal agent configuration directory. - public static AgentDiscoveryPathScope User { get; } = new("user"); + /// Paths use Windows path conventions. + public static SessionFsSetProviderConventions Windows { get; } = new("windows"); - /// A project's repository agent directory. - public static AgentDiscoveryPathScope Project { get; } = new("project"); + /// Paths use POSIX path conventions. + public static SessionFsSetProviderConventions Posix { get; } = new("posix"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentDiscoveryPathScope left, AgentDiscoveryPathScope right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionFsSetProviderConventions left, SessionFsSetProviderConventions right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentDiscoveryPathScope left, AgentDiscoveryPathScope right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionFsSetProviderConventions left, SessionFsSetProviderConventions right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentDiscoveryPathScope other && Equals(other); + public override bool Equals(object? obj) => obj is SessionFsSetProviderConventions other && Equals(other); /// - public bool Equals(AgentDiscoveryPathScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionFsSetProviderConventions other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30076,68 +32445,62 @@ public AgentDiscoveryPathScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentDiscoveryPathScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionFsSetProviderConventions Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentDiscoveryPathScope value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionFsSetProviderConventions value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentDiscoveryPathScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionFsSetProviderConventions)); } } } -/// Where this source lives — used for UI grouping. +/// Repository host type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct InstructionSourceLocation : IEquatable +public readonly struct SessionContextHostType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public InstructionSourceLocation(string value) + public SessionContextHostType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Instructions live in user-level configuration. - public static InstructionSourceLocation User { get; } = new("user"); - - /// Instructions live in repository-level configuration. - public static InstructionSourceLocation Repository { get; } = new("repository"); - - /// Instructions live under the current working directory. - public static InstructionSourceLocation WorkingDirectory { get; } = new("working-directory"); + /// Session repository is hosted on GitHub. + public static SessionContextHostType GitHub { get; } = new("github"); - /// Instructions live in plugin-provided configuration. - public static InstructionSourceLocation Plugin { get; } = new("plugin"); + /// Session repository is hosted on Azure DevOps. + public static SessionContextHostType Ado { get; } = new("ado"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(InstructionSourceLocation left, InstructionSourceLocation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionContextHostType left, SessionContextHostType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(InstructionSourceLocation left, InstructionSourceLocation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionContextHostType left, SessionContextHostType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is InstructionSourceLocation other && Equals(other); + public override bool Equals(object? obj) => obj is SessionContextHostType other && Equals(other); /// - public bool Equals(InstructionSourceLocation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30145,77 +32508,68 @@ public InstructionSourceLocation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override InstructionSourceLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, InstructionSourceLocation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionContextHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceLocation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionContextHostType)); } } } -/// Category of instruction source — used for merge logic. +/// What a remote host says one of its sessions is doing right now. Deliberately coarse: this is what a host can report for EVERY session in a catalogue listing, without a client subscribing to each one. AHP's `SessionSummary.status` is the source today; `input-needed` covers both a permission prompt and an `ask_user` question, since the summary does not say which. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct InstructionSourceType : IEquatable +public readonly struct RemoteSessionHostStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public InstructionSourceType(string value) + public RemoteSessionHostStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Instructions loaded from the user's home configuration. - public static InstructionSourceType Home { get; } = new("home"); - - /// Instructions loaded from repository-scoped files. - public static InstructionSourceType Repo { get; } = new("repo"); - - /// Instructions loaded from model-specific files. - public static InstructionSourceType Model { get; } = new("model"); - - /// Instructions loaded from VS Code instruction files. - public static InstructionSourceType Vscode { get; } = new("vscode"); + /// No turn is running. + public static RemoteSessionHostStatus Idle { get; } = new("idle"); - /// Instructions discovered from nested agent files. - public static InstructionSourceType NestedAgents { get; } = new("nested-agents"); + /// A turn is running. + public static RemoteSessionHostStatus Working { get; } = new("working"); - /// Instructions inherited from child instruction files. - public static InstructionSourceType ChildInstructions { get; } = new("child-instructions"); + /// The session is blocked on the user: a permission prompt or an `ask_user` question. + public static RemoteSessionHostStatus InputNeeded { get; } = new("input-needed"); - /// Instructions supplied by an installed plugin. - public static InstructionSourceType Plugin { get; } = new("plugin"); + /// The session ended its last turn in an error. + public static RemoteSessionHostStatus Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(InstructionSourceType left, InstructionSourceType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(RemoteSessionHostStatus left, RemoteSessionHostStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(InstructionSourceType left, InstructionSourceType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(RemoteSessionHostStatus left, RemoteSessionHostStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is InstructionSourceType other && Equals(other); + public override bool Equals(object? obj) => obj is RemoteSessionHostStatus other && Equals(other); /// - public bool Equals(InstructionSourceType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(RemoteSessionHostStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30223,62 +32577,62 @@ public InstructionSourceType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override InstructionSourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override RemoteSessionHostStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, InstructionSourceType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, RemoteSessionHostStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionHostStatus)); } } } -/// Whether the target is a single file or a directory of instruction files. +/// Whether the remote task originated from CCA or CLI `--remote`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct InstructionDiscoveryPathKind : IEquatable +public readonly struct RemoteSessionMetadataTaskType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public InstructionDiscoveryPathKind(string value) + public RemoteSessionMetadataTaskType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The target is a single instruction file. - public static InstructionDiscoveryPathKind File { get; } = new("file"); + /// GitHub Copilot coding agent task. + public static RemoteSessionMetadataTaskType Cca { get; } = new("cca"); - /// The target is a directory that holds instruction files. - public static InstructionDiscoveryPathKind Directory { get; } = new("directory"); + /// CLI remote task. + public static RemoteSessionMetadataTaskType Cli { get; } = new("cli"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(InstructionDiscoveryPathKind left, InstructionDiscoveryPathKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(InstructionDiscoveryPathKind left, InstructionDiscoveryPathKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is InstructionDiscoveryPathKind other && Equals(other); + public override bool Equals(object? obj) => obj is RemoteSessionMetadataTaskType other && Equals(other); /// - public bool Equals(InstructionDiscoveryPathKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(RemoteSessionMetadataTaskType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30286,68 +32640,62 @@ public InstructionDiscoveryPathKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override InstructionDiscoveryPathKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override RemoteSessionMetadataTaskType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, InstructionDiscoveryPathKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, RemoteSessionMetadataTaskType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionMetadataTaskType)); } } } -/// Which tier this target belongs to. +/// Step status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct InstructionDiscoveryPathLocation : IEquatable +public readonly struct SessionsOpenProgressStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public InstructionDiscoveryPathLocation(string value) + public SessionsOpenProgressStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Instructions live in user-level configuration. - public static InstructionDiscoveryPathLocation User { get; } = new("user"); - - /// Instructions live in repository-level configuration. - public static InstructionDiscoveryPathLocation Repository { get; } = new("repository"); - - /// Instructions live under the current working directory. - public static InstructionDiscoveryPathLocation WorkingDirectory { get; } = new("working-directory"); + /// The step has started and has not yet finished. + public static SessionsOpenProgressStatus InProgress { get; } = new("in-progress"); - /// Instructions live in plugin-provided configuration. - public static InstructionDiscoveryPathLocation Plugin { get; } = new("plugin"); + /// The step has completed successfully. + public static SessionsOpenProgressStatus Complete { get; } = new("complete"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is InstructionDiscoveryPathLocation other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenProgressStatus other && Equals(other); /// - public bool Equals(InstructionDiscoveryPathLocation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionsOpenProgressStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30355,59 +32703,74 @@ public InstructionDiscoveryPathLocation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override InstructionDiscoveryPathLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionsOpenProgressStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, InstructionDiscoveryPathLocation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionsOpenProgressStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathLocation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStatus)); } } } -/// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). +/// Handoff step. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SlashCommandInputCompletion : IEquatable +public readonly struct SessionsOpenProgressStep : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SlashCommandInputCompletion(string value) + public SessionsOpenProgressStep(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Input should complete filesystem directories. - public static SlashCommandInputCompletion Directory { get; } = new("directory"); + /// Loading the source session's events from the remote service. + public static SessionsOpenProgressStep LoadSession { get; } = new("load-session"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => left.Equals(right); + /// Validating that the local repository matches the remote session's repository. + public static SessionsOpenProgressStep ValidateRepo { get; } = new("validate-repo"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => !(left == right); + /// Checking the local working tree for uncommitted changes that would block the handoff. + public static SessionsOpenProgressStep CheckChanges { get; } = new("check-changes"); + + /// Checking out the branch associated with the remote session in the local working tree. + public static SessionsOpenProgressStep CheckoutBranch { get; } = new("checkout-branch"); + + /// Creating the new local session and seeding it with the source session's events. + public static SessionsOpenProgressStep CreateSession { get; } = new("create-session"); + + /// Persisting the newly-created local session to disk. + public static SessionsOpenProgressStep SaveSession { get; } = new("save-session"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenProgressStep other && Equals(other); /// - public bool Equals(SlashCommandInputCompletion other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionsOpenProgressStep other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30415,65 +32778,71 @@ public SlashCommandInputCompletion(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SlashCommandInputCompletion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionsOpenProgressStep Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SlashCommandInputCompletion value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionsOpenProgressStep value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStep)); } } } -/// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. +/// Outcome of the open request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SlashCommandKind : IEquatable +public readonly struct SessionsOpenStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SlashCommandKind(string value) + public SessionsOpenStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Command implemented by the runtime. - public static SlashCommandKind Builtin { get; } = new("builtin"); + /// A new session was created. + public static SessionsOpenStatus Created { get; } = new("created"); - /// Command backed by a skill. - public static SlashCommandKind Skill { get; } = new("skill"); + /// An existing session was loaded or reattached. + public static SessionsOpenStatus Resumed { get; } = new("resumed"); - /// Command registered by an SDK client or extension. - public static SlashCommandKind Client { get; } = new("client"); + /// No matching persisted session was found. + public static SessionsOpenStatus NotFound { get; } = new("not_found"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SlashCommandKind left, SlashCommandKind right) => left.Equals(right); + /// Connected to an existing remote session. + public static SessionsOpenStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SlashCommandKind left, SlashCommandKind right) => !(left == right); + /// Remote session was handed off to a new local session. + public static SessionsOpenStatus HandedOff { get; } = new("handed_off"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionsOpenStatus left, SessionsOpenStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionsOpenStatus left, SessionsOpenStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenStatus other && Equals(other); /// - public bool Equals(SlashCommandKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionsOpenStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30481,62 +32850,62 @@ public SlashCommandKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SlashCommandKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionsOpenStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SlashCommandKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionsOpenStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenStatus)); } } } -/// Severity of a managed-settings validation finding. +/// Neutral SDK discriminator for the connected remote session kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ManagedSettingsDiagnosticSeverity : IEquatable +public readonly struct ConnectedRemoteSessionMetadataKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ManagedSettingsDiagnosticSeverity(string value) + public ConnectedRemoteSessionMetadataKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The runtime rejects the document. - public static ManagedSettingsDiagnosticSeverity Error { get; } = new("error"); + /// Remote CLI session. + public static ConnectedRemoteSessionMetadataKind RemoteSession { get; } = new("remote-session"); - /// The runtime accepts the document but ignores the flagged content. - public static ManagedSettingsDiagnosticSeverity Warning { get; } = new("warning"); + /// GitHub Copilot coding agent session. + public static ConnectedRemoteSessionMetadataKind CodingAgent { get; } = new("coding-agent"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ManagedSettingsDiagnosticSeverity left, ManagedSettingsDiagnosticSeverity right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ManagedSettingsDiagnosticSeverity left, ManagedSettingsDiagnosticSeverity right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ManagedSettingsDiagnosticSeverity other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectedRemoteSessionMetadataKind other && Equals(other); /// - public bool Equals(ManagedSettingsDiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectedRemoteSessionMetadataKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30544,65 +32913,65 @@ public ManagedSettingsDiagnosticSeverity(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ManagedSettingsDiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectedRemoteSessionMetadataKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ManagedSettingsDiagnosticSeverity value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectedRemoteSessionMetadataKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsDiagnosticSeverity)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectedRemoteSessionMetadataKind)); } } } -/// A channel accepted by managedSettings.compose. +/// Which session sources to include. Defaults to `local` for backward compatibility. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ManagedSettingsChannel : IEquatable +public readonly struct SessionSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ManagedSettingsChannel(string value) + public SessionSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Device policy, the strongest channel. - public static ManagedSettingsChannel Device { get; } = new("device"); + /// Return only local sessions. + public static SessionSource Local { get; } = new("local"); - /// Account or organization policy. - public static ManagedSettingsChannel Server { get; } = new("server"); + /// Return only remote sessions. + public static SessionSource Remote { get; } = new("remote"); - /// Session-local helper output, the weakest channel. - public static ManagedSettingsChannel PolicyHelper { get; } = new("policyHelper"); + /// Return both local and remote sessions. + public static SessionSource All { get; } = new("all"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ManagedSettingsChannel left, ManagedSettingsChannel right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionSource left, SessionSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ManagedSettingsChannel left, ManagedSettingsChannel right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionSource left, SessionSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ManagedSettingsChannel other && Equals(other); + public override bool Equals(object? obj) => obj is SessionSource other && Equals(other); /// - public bool Equals(ManagedSettingsChannel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30610,62 +32979,62 @@ public ManagedSettingsChannel(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ManagedSettingsChannel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ManagedSettingsChannel value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsChannel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionSource)); } } } -/// Path conventions used by this filesystem. +/// Cursor status: 'ok' means the read succeeded against the requested history; 'expired' means the requested continuation is unavailable. Recovery is endpoint-specific: session.eventLog.read returns a boundary window of remaining active history that may overlap prior pages, while sessions.readPersistedEvents returns an empty terminal page and never switches journal generations. An expired persisted read is not successful completion; a complete persisted snapshot requires cursorStatus 'ok' and hasMore false. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionFsSetProviderConventions : IEquatable +public readonly struct EventsCursorStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionFsSetProviderConventions(string value) + public EventsCursorStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Paths use Windows path conventions. - public static SessionFsSetProviderConventions Windows { get; } = new("windows"); + /// The read succeeded against the requested history. + public static EventsCursorStatus Ok { get; } = new("ok"); - /// Paths use POSIX path conventions. - public static SessionFsSetProviderConventions Posix { get; } = new("posix"); + /// The requested continuation is unavailable; see the endpoint's recovery semantics. + public static EventsCursorStatus Expired { get; } = new("expired"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionFsSetProviderConventions left, SessionFsSetProviderConventions right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionFsSetProviderConventions left, SessionFsSetProviderConventions right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionFsSetProviderConventions other && Equals(other); + public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); /// - public bool Equals(SessionFsSetProviderConventions other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(EventsCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30673,62 +33042,62 @@ public SessionFsSetProviderConventions(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionFsSetProviderConventions Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionFsSetProviderConventions value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionFsSetProviderConventions)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); } } } -/// Repository host type. +/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionContextHostType : IEquatable +public readonly struct EventsReadDirection : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionContextHostType(string value) + public EventsReadDirection(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Session repository is hosted on GitHub. - public static SessionContextHostType GitHub { get; } = new("github"); + /// Page from the cursor toward newer events (default). + public static EventsReadDirection Forward { get; } = new("forward"); - /// Session repository is hosted on Azure DevOps. - public static SessionContextHostType Ado { get; } = new("ado"); + /// Tail-first: return the newest events and page toward older events. + public static EventsReadDirection Backward { get; } = new("backward"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionContextHostType left, SessionContextHostType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionContextHostType left, SessionContextHostType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionContextHostType other && Equals(other); + public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); /// - public bool Equals(SessionContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(EventsReadDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30736,68 +33105,71 @@ public SessionContextHostType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionContextHostType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionContextHostType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); } } } -/// What a remote host says one of its sessions is doing right now. Deliberately coarse: this is what a host can report for EVERY session in a catalogue listing, without a client subscribing to each one. AHP's `SessionSummary.status` is the source today; `input-needed` covers both a permission prompt and an `ask_user` question, since the summary does not say which. +/// Kind of attention required when status === "attention". Meaningful only when status === "attention". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct RemoteSessionHostStatus : IEquatable +public readonly struct AgentRegistryLiveTargetEntryAttentionKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public RemoteSessionHostStatus(string value) + public AgentRegistryLiveTargetEntryAttentionKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No turn is running. - public static RemoteSessionHostStatus Idle { get; } = new("idle"); + /// Session is blocked on an unrecoverable error. + public static AgentRegistryLiveTargetEntryAttentionKind Error { get; } = new("error"); - /// A turn is running. - public static RemoteSessionHostStatus Working { get; } = new("working"); + /// Session is waiting for a tool-permission decision. + public static AgentRegistryLiveTargetEntryAttentionKind Permission { get; } = new("permission"); - /// The session is blocked on the user: a permission prompt or an `ask_user` question. - public static RemoteSessionHostStatus InputNeeded { get; } = new("input-needed"); + /// Session is waiting for the user to approve or reject a plan. + public static AgentRegistryLiveTargetEntryAttentionKind ExitPlan { get; } = new("exit_plan"); - /// The session ended its last turn in an error. - public static RemoteSessionHostStatus Error { get; } = new("error"); + /// Session is waiting on an elicitation prompt. + public static AgentRegistryLiveTargetEntryAttentionKind Elicitation { get; } = new("elicitation"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(RemoteSessionHostStatus left, RemoteSessionHostStatus right) => left.Equals(right); + /// Session is waiting for free-form user input. + public static AgentRegistryLiveTargetEntryAttentionKind UserInput { get; } = new("user_input"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(RemoteSessionHostStatus left, RemoteSessionHostStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is RemoteSessionHostStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryAttentionKind other && Equals(other); /// - public bool Equals(RemoteSessionHostStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistryLiveTargetEntryAttentionKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30805,62 +33177,62 @@ public RemoteSessionHostStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override RemoteSessionHostStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistryLiveTargetEntryAttentionKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, RemoteSessionHostStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryAttentionKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionHostStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryAttentionKind)); } } } -/// Whether the remote task originated from CCA or CLI `--remote`. +/// Process kind tag for the registry entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct RemoteSessionMetadataTaskType : IEquatable +public readonly struct AgentRegistryLiveTargetEntryKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public RemoteSessionMetadataTaskType(string value) + public AgentRegistryLiveTargetEntryKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// GitHub Copilot coding agent task. - public static RemoteSessionMetadataTaskType Cca { get; } = new("cca"); + /// Interactive Copilot CLI exposing a UI server (legacy/normal CLI process). + public static AgentRegistryLiveTargetEntryKind UiServer { get; } = new("ui-server"); - /// CLI remote task. - public static RemoteSessionMetadataTaskType Cli { get; } = new("cli"); + /// Headless `--server --managed-server` child spawned by a controller. + public static AgentRegistryLiveTargetEntryKind ManagedServer { get; } = new("managed-server"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistryLiveTargetEntryKind left, AgentRegistryLiveTargetEntryKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistryLiveTargetEntryKind left, AgentRegistryLiveTargetEntryKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is RemoteSessionMetadataTaskType other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryKind other && Equals(other); /// - public bool Equals(RemoteSessionMetadataTaskType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistryLiveTargetEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30868,62 +33240,62 @@ public RemoteSessionMetadataTaskType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override RemoteSessionMetadataTaskType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistryLiveTargetEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, RemoteSessionMetadataTaskType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionMetadataTaskType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryKind)); } } } -/// Step status. +/// How the most recent turn ended (clean vs aborted). Lets the renderer distinguish done from done_cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionsOpenProgressStatus : IEquatable +public readonly struct AgentRegistryLiveTargetEntryLastTerminalEvent : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionsOpenProgressStatus(string value) + public AgentRegistryLiveTargetEntryLastTerminalEvent(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The step has started and has not yet finished. - public static SessionsOpenProgressStatus InProgress { get; } = new("in-progress"); + /// Last turn ended cleanly (model returned a final assistant message). + public static AgentRegistryLiveTargetEntryLastTerminalEvent TurnEnd { get; } = new("turn_end"); - /// The step has completed successfully. - public static SessionsOpenProgressStatus Complete { get; } = new("complete"); + /// Last turn was aborted (e.g. user interrupted). + public static AgentRegistryLiveTargetEntryLastTerminalEvent Abort { get; } = new("abort"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionsOpenProgressStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryLastTerminalEvent other && Equals(other); /// - public bool Equals(SessionsOpenProgressStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistryLiveTargetEntryLastTerminalEvent other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -30931,74 +33303,68 @@ public SessionsOpenProgressStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionsOpenProgressStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistryLiveTargetEntryLastTerminalEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionsOpenProgressStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryLastTerminalEvent value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryLastTerminalEvent)); } } } -/// Handoff step. +/// Coarse lifecycle status of the foreground session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionsOpenProgressStep : IEquatable +public readonly struct AgentRegistryLiveTargetEntryStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionsOpenProgressStep(string value) + public AgentRegistryLiveTargetEntryStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Loading the source session's events from the remote service. - public static SessionsOpenProgressStep LoadSession { get; } = new("load-session"); - - /// Validating that the local repository matches the remote session's repository. - public static SessionsOpenProgressStep ValidateRepo { get; } = new("validate-repo"); - - /// Checking the local working tree for uncommitted changes that would block the handoff. - public static SessionsOpenProgressStep CheckChanges { get; } = new("check-changes"); + /// Session is actively processing a turn. + public static AgentRegistryLiveTargetEntryStatus Working { get; } = new("working"); - /// Checking out the branch associated with the remote session in the local working tree. - public static SessionsOpenProgressStep CheckoutBranch { get; } = new("checkout-branch"); + /// Session is idle, waiting for input. + public static AgentRegistryLiveTargetEntryStatus Waiting { get; } = new("waiting"); - /// Creating the new local session and seeding it with the source session's events. - public static SessionsOpenProgressStep CreateSession { get; } = new("create-session"); + /// Last turn completed successfully. + public static AgentRegistryLiveTargetEntryStatus Done { get; } = new("done"); - /// Persisting the newly-created local session to disk. - public static SessionsOpenProgressStep SaveSession { get; } = new("save-session"); + /// Session needs user attention (see attentionKind for the specific reason). + public static AgentRegistryLiveTargetEntryStatus Attention { get; } = new("attention"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistryLiveTargetEntryStatus left, AgentRegistryLiveTargetEntryStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionsOpenProgressStep left, SessionsOpenProgressStep right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistryLiveTargetEntryStatus left, AgentRegistryLiveTargetEntryStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionsOpenProgressStep other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryStatus other && Equals(other); /// - public bool Equals(SessionsOpenProgressStep other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistryLiveTargetEntryStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31006,71 +33372,65 @@ public SessionsOpenProgressStep(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionsOpenProgressStep Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistryLiveTargetEntryStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionsOpenProgressStep value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStep)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryStatus)); } } } -/// Outcome of the open request. +/// Categorized reason no canonical process log could be opened. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionsOpenStatus : IEquatable +public readonly struct AgentRegistryLogCaptureOpenErrorReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionsOpenStatus(string value) + public AgentRegistryLogCaptureOpenErrorReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A new session was created. - public static SessionsOpenStatus Created { get; } = new("created"); - - /// An existing session was loaded or reattached. - public static SessionsOpenStatus Resumed { get; } = new("resumed"); - - /// No matching persisted session was found. - public static SessionsOpenStatus NotFound { get; } = new("not_found"); + /// Filesystem permission denied opening the log file. + public static AgentRegistryLogCaptureOpenErrorReason Permission { get; } = new("permission"); - /// Connected to an existing remote session. - public static SessionsOpenStatus Connected { get; } = new("connected"); + /// No space left on device. + public static AgentRegistryLogCaptureOpenErrorReason DiskFull { get; } = new("disk_full"); - /// Remote session was handed off to a new local session. - public static SessionsOpenStatus HandedOff { get; } = new("handed_off"); + /// Other / uncategorized open failure. + public static AgentRegistryLogCaptureOpenErrorReason Other { get; } = new("other"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionsOpenStatus left, SessionsOpenStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistryLogCaptureOpenErrorReason left, AgentRegistryLogCaptureOpenErrorReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionsOpenStatus left, SessionsOpenStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistryLogCaptureOpenErrorReason left, AgentRegistryLogCaptureOpenErrorReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionsOpenStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLogCaptureOpenErrorReason other && Equals(other); /// - public bool Equals(SessionsOpenStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistryLogCaptureOpenErrorReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31078,62 +33438,71 @@ public SessionsOpenStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionsOpenStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistryLogCaptureOpenErrorReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionsOpenStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistryLogCaptureOpenErrorReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLogCaptureOpenErrorReason)); } } } -/// Neutral SDK discriminator for the connected remote session kind. +/// Which parameter field was invalid. Omitted when the rejection is not field-specific. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectedRemoteSessionMetadataKind : IEquatable +public readonly struct AgentRegistrySpawnValidationErrorField : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectedRemoteSessionMetadataKind(string value) + public AgentRegistrySpawnValidationErrorField(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Remote CLI session. - public static ConnectedRemoteSessionMetadataKind RemoteSession { get; } = new("remote-session"); + /// The cwd parameter. + public static AgentRegistrySpawnValidationErrorField Cwd { get; } = new("cwd"); - /// GitHub Copilot coding agent session. - public static ConnectedRemoteSessionMetadataKind CodingAgent { get; } = new("coding-agent"); + /// The session name parameter. + public static AgentRegistrySpawnValidationErrorField Name { get; } = new("name"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind right) => left.Equals(right); + /// The agentName parameter. + public static AgentRegistrySpawnValidationErrorField AgentName { get; } = new("agentName"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind right) => !(left == right); + /// The model parameter. + public static AgentRegistrySpawnValidationErrorField Model { get; } = new("model"); + + /// The permissionMode parameter. + public static AgentRegistrySpawnValidationErrorField PermissionMode { get; } = new("permissionMode"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectedRemoteSessionMetadataKind other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorField other && Equals(other); /// - public bool Equals(ConnectedRemoteSessionMetadataKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistrySpawnValidationErrorField other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31141,65 +33510,74 @@ public ConnectedRemoteSessionMetadataKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectedRemoteSessionMetadataKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistrySpawnValidationErrorField Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectedRemoteSessionMetadataKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorField value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectedRemoteSessionMetadataKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorField)); } } } -/// Which session sources to include. Defaults to `local` for backward compatibility. +/// Categorized reason for the rejection. Low-cardinality enum so telemetry can aggregate by reason without leaking raw paths or agent/model names. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionSource : IEquatable +public readonly struct AgentRegistrySpawnValidationErrorReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionSource(string value) + public AgentRegistrySpawnValidationErrorReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Return only local sessions. - public static SessionSource Local { get; } = new("local"); + /// Provided cwd does not exist on disk. + public static AgentRegistrySpawnValidationErrorReason CwdNotFound { get; } = new("cwd-not-found"); - /// Return only remote sessions. - public static SessionSource Remote { get; } = new("remote"); + /// Provided cwd exists but is not a directory. + public static AgentRegistrySpawnValidationErrorReason CwdNotDirectory { get; } = new("cwd-not-directory"); - /// Return both local and remote sessions. - public static SessionSource All { get; } = new("all"); + /// Session name failed validateSessionName. + public static AgentRegistrySpawnValidationErrorReason InvalidName { get; } = new("invalid-name"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionSource left, SessionSource right) => left.Equals(right); + /// Requested agent name was not found in builtin or custom agents. + public static AgentRegistrySpawnValidationErrorReason UnknownAgent { get; } = new("unknown-agent"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionSource left, SessionSource right) => !(left == right); + /// Requested model is not available to this session. + public static AgentRegistrySpawnValidationErrorReason UnknownModel { get; } = new("unknown-model"); + + /// Caller asked for permissionMode='yolo' but the controller is not currently in allow-all mode. + public static AgentRegistrySpawnValidationErrorReason YoloNotAllowed { get; } = new("yolo-not-allowed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionSource other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorReason other && Equals(other); /// - public bool Equals(SessionSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistrySpawnValidationErrorReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31207,62 +33585,62 @@ public SessionSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistrySpawnValidationErrorReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorReason)); } } } -/// Cursor status: 'ok' means the read succeeded against the requested history; 'expired' means the requested continuation is unavailable. Recovery is endpoint-specific: session.eventLog.read returns a boundary window of remaining active history that may overlap prior pages, while sessions.readPersistedEvents returns an empty terminal page and never switches journal generations. An expired persisted read is not successful completion; a complete persisted snapshot requires cursorStatus 'ok' and hasMore false. +/// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct EventsCursorStatus : IEquatable +public readonly struct AgentRegistrySpawnPermissionMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public EventsCursorStatus(string value) + public AgentRegistrySpawnPermissionMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The read succeeded against the requested history. - public static EventsCursorStatus Ok { get; } = new("ok"); + /// Standard permission posture (prompts for each request). + public static AgentRegistrySpawnPermissionMode Default { get; } = new("default"); - /// The requested continuation is unavailable; see the endpoint's recovery semantics. - public static EventsCursorStatus Expired { get; } = new("expired"); + /// Full allow-all (requires the controller-local session to currently be in allow-all mode). + public static AgentRegistrySpawnPermissionMode Yolo { get; } = new("yolo"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistrySpawnPermissionMode other && Equals(other); /// - public bool Equals(EventsCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AgentRegistrySpawnPermissionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31270,62 +33648,65 @@ public EventsCursorStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AgentRegistrySpawnPermissionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnPermissionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnPermissionMode)); } } } -/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. +/// Availability. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct EventsReadDirection : IEquatable +public readonly struct ConnectorDiscoveryAvailability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public EventsReadDirection(string value) + public ConnectorDiscoveryAvailability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Page from the cursor toward newer events (default). - public static EventsReadDirection Forward { get; } = new("forward"); + /// Enabled. + public static ConnectorDiscoveryAvailability Enabled { get; } = new("enabled"); - /// Tail-first: return the newest events and page toward older events. - public static EventsReadDirection Backward { get; } = new("backward"); + /// Disabled. + public static ConnectorDiscoveryAvailability Disabled { get; } = new("disabled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); + /// Unavailable. + public static ConnectorDiscoveryAvailability Unavailable { get; } = new("unavailable"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorDiscoveryAvailability left, ConnectorDiscoveryAvailability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorDiscoveryAvailability other && Equals(other); /// - public bool Equals(EventsReadDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorDiscoveryAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31333,71 +33714,83 @@ public EventsReadDirection(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorDiscoveryAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorDiscoveryAvailability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorDiscoveryAvailability)); } } } -/// Kind of attention required when status === "attention". Meaningful only when status === "attention". +/// Authentication type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryAttentionKind : IEquatable +public readonly struct AuthInfoType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistryLiveTargetEntryAttentionKind(string value) + public AuthInfoType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Session is blocked on an unrecoverable error. - public static AgentRegistryLiveTargetEntryAttentionKind Error { get; } = new("error"); + /// Authentication provided by a GitHub App HMAC credential. + public static AuthInfoType Hmac { get; } = new("hmac"); - /// Session is waiting for a tool-permission decision. - public static AgentRegistryLiveTargetEntryAttentionKind Permission { get; } = new("permission"); + /// Authentication resolved from environment-provided credentials. + public static AuthInfoType Env { get; } = new("env"); - /// Session is waiting for the user to approve or reject a plan. - public static AgentRegistryLiveTargetEntryAttentionKind ExitPlan { get; } = new("exit_plan"); + /// Authentication from an interactive user sign-in. + public static AuthInfoType User { get; } = new("user"); - /// Session is waiting on an elicitation prompt. - public static AgentRegistryLiveTargetEntryAttentionKind Elicitation { get; } = new("elicitation"); + /// Authentication from a selected provider-owned account, without a GitHub credential. + public static AuthInfoType Account { get; } = new("account"); - /// Session is waiting for free-form user input. - public static AgentRegistryLiveTargetEntryAttentionKind UserInput { get; } = new("user_input"); + /// Authentication delegated to the GitHub CLI. + public static AuthInfoType GhCli { get; } = new("gh-cli"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind right) => left.Equals(right); + /// Authentication from an API key credential. + public static AuthInfoType ApiKey { get; } = new("api-key"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind right) => !(left == right); + /// Authentication from a GitHub token. + public static AuthInfoType Token { get; } = new("token"); + + /// Authentication from an SDK GitHub token callback. + public static AuthInfoType TokenProvider { get; } = new("token-provider"); + + /// Authentication from a Copilot API token. + public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryAttentionKind other && Equals(other); + public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryAttentionKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AuthInfoType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31405,62 +33798,71 @@ public AgentRegistryLiveTargetEntryAttentionKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistryLiveTargetEntryAttentionKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryAttentionKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryAttentionKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); } } } -/// Process kind tag for the registry entry. +/// Authoritative service connection state for one Connector. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryKind : IEquatable +public readonly struct ConnectorCatalogStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistryLiveTargetEntryKind(string value) + public ConnectorCatalogStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Interactive Copilot CLI exposing a UI server (legacy/normal CLI process). - public static AgentRegistryLiveTargetEntryKind UiServer { get; } = new("ui-server"); + /// The Connector is available but not connected. + public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - /// Headless `--server --managed-server` child spawned by a controller. - public static AgentRegistryLiveTargetEntryKind ManagedServer { get; } = new("managed-server"); + /// The Connector service is still completing connection or consent. + public static ConnectorCatalogStatus Pending { get; } = new("pending"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistryLiveTargetEntryKind left, AgentRegistryLiveTargetEntryKind right) => left.Equals(right); + /// The Connector is connected and may contribute MCP servers. + public static ConnectorCatalogStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistryLiveTargetEntryKind left, AgentRegistryLiveTargetEntryKind right) => !(left == right); + /// The Connector service reports an unusable connection. + public static ConnectorCatalogStatus Error { get; } = new("error"); + + /// The service returned a future or unrecognized state. + public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryKind other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31468,62 +33870,68 @@ public AgentRegistryLiveTargetEntryKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistryLiveTargetEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); } } } -/// How the most recent turn ended (clean vs aborted). Lets the renderer distinguish done from done_cancelled. +/// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryLastTerminalEvent : IEquatable +public readonly struct SendAgentMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistryLiveTargetEntryLastTerminalEvent(string value) + public SendAgentMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The agent is responding interactively to the user. + public static SendAgentMode Interactive { get; } = new("interactive"); + + /// The agent is preparing a plan before making changes. + public static SendAgentMode Plan { get; } = new("plan"); - /// Last turn ended cleanly (model returned a final assistant message). - public static AgentRegistryLiveTargetEntryLastTerminalEvent TurnEnd { get; } = new("turn_end"); + /// The agent is working autonomously toward task completion. + public static SendAgentMode Autopilot { get; } = new("autopilot"); - /// Last turn was aborted (e.g. user interrupted). - public static AgentRegistryLiveTargetEntryLastTerminalEvent Abort { get; } = new("abort"); + /// The agent is in shell-focused UI mode. + public static SendAgentMode Shell { get; } = new("shell"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SendAgentMode left, SendAgentMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SendAgentMode left, SendAgentMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryLastTerminalEvent other && Equals(other); + public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryLastTerminalEvent other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SendAgentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31531,68 +33939,62 @@ public AgentRegistryLiveTargetEntryLastTerminalEvent(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistryLiveTargetEntryLastTerminalEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SendAgentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryLastTerminalEvent value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SendAgentMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryLastTerminalEvent)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); } } } -/// Coarse lifecycle status of the foreground session. +/// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryStatus : IEquatable +public readonly struct SendMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistryLiveTargetEntryStatus(string value) + public SendMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Session is actively processing a turn. - public static AgentRegistryLiveTargetEntryStatus Working { get; } = new("working"); - - /// Session is idle, waiting for input. - public static AgentRegistryLiveTargetEntryStatus Waiting { get; } = new("waiting"); - - /// Last turn completed successfully. - public static AgentRegistryLiveTargetEntryStatus Done { get; } = new("done"); + /// Append the message to the normal session queue. + public static SendMode Enqueue { get; } = new("enqueue"); - /// Session needs user attention (see attentionKind for the specific reason). - public static AgentRegistryLiveTargetEntryStatus Attention { get; } = new("attention"); + /// Interject the message during the in-progress turn. + public static SendMode Immediate { get; } = new("immediate"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistryLiveTargetEntryStatus left, AgentRegistryLiveTargetEntryStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SendMode left, SendMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistryLiveTargetEntryStatus left, AgentRegistryLiveTargetEntryStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SendMode left, SendMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryStatus other && Equals(other); + public override bool Equals(object? obj) => obj is SendMode other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31600,65 +34002,65 @@ public AgentRegistryLiveTargetEntryStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistryLiveTargetEntryStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistryLiveTargetEntryStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SendMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); } } } -/// Categorized reason no canonical process log could be opened. +/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLogCaptureOpenErrorReason : IEquatable +public readonly struct SessionLogLevel : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistryLogCaptureOpenErrorReason(string value) + public SessionLogLevel(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Filesystem permission denied opening the log file. - public static AgentRegistryLogCaptureOpenErrorReason Permission { get; } = new("permission"); + /// Informational message. + public static SessionLogLevel Info { get; } = new("info"); - /// No space left on device. - public static AgentRegistryLogCaptureOpenErrorReason DiskFull { get; } = new("disk_full"); + /// Warning message that may require attention. + public static SessionLogLevel Warning { get; } = new("warning"); - /// Other / uncategorized open failure. - public static AgentRegistryLogCaptureOpenErrorReason Other { get; } = new("other"); + /// Error message describing a failure. + public static SessionLogLevel Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistryLogCaptureOpenErrorReason left, AgentRegistryLogCaptureOpenErrorReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionLogLevel left, SessionLogLevel right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistryLogCaptureOpenErrorReason left, AgentRegistryLogCaptureOpenErrorReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionLogLevel left, SessionLogLevel right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLogCaptureOpenErrorReason other && Equals(other); + public override bool Equals(object? obj) => obj is SessionLogLevel other && Equals(other); /// - public bool Equals(AgentRegistryLogCaptureOpenErrorReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(SessionLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31666,71 +34068,65 @@ public AgentRegistryLogCaptureOpenErrorReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistryLogCaptureOpenErrorReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SessionLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistryLogCaptureOpenErrorReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SessionLogLevel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLogCaptureOpenErrorReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); } } } -/// Which parameter field was invalid. Omitted when the rejection is not field-specific. +/// When the runtime may run an adapter without an explicit user action. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnValidationErrorField : IEquatable +public readonly struct ModelProviderAutomaticDiscoveryMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistrySpawnValidationErrorField(string value) + public ModelProviderAutomaticDiscoveryMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The cwd parameter. - public static AgentRegistrySpawnValidationErrorField Cwd { get; } = new("cwd"); - - /// The session name parameter. - public static AgentRegistrySpawnValidationErrorField Name { get; } = new("name"); - - /// The agentName parameter. - public static AgentRegistrySpawnValidationErrorField AgentName { get; } = new("agentName"); + /// The adapter declares that automatic discovery is safe when the other policy fields are satisfied. + public static ModelProviderAutomaticDiscoveryMode Automatic { get; } = new("automatic"); - /// The model parameter. - public static AgentRegistrySpawnValidationErrorField Model { get; } = new("model"); + /// The adapter may refresh instances the user already configured, but must not scan for new instances automatically. + public static ModelProviderAutomaticDiscoveryMode ConfiguredOnly { get; } = new("configuredOnly"); - /// The permissionMode parameter. - public static AgentRegistrySpawnValidationErrorField PermissionMode { get; } = new("permissionMode"); + /// The adapter must run only after an explicit user action. + public static ModelProviderAutomaticDiscoveryMode Explicit { get; } = new("explicit"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderAutomaticDiscoveryMode left, ModelProviderAutomaticDiscoveryMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistrySpawnValidationErrorField left, AgentRegistrySpawnValidationErrorField right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderAutomaticDiscoveryMode left, ModelProviderAutomaticDiscoveryMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorField other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderAutomaticDiscoveryMode other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorField other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderAutomaticDiscoveryMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31738,74 +34134,71 @@ public AgentRegistrySpawnValidationErrorField(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistrySpawnValidationErrorField Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderAutomaticDiscoveryMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorField value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderAutomaticDiscoveryMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorField)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderAutomaticDiscoveryMode)); } } } -/// Categorized reason for the rejection. Low-cardinality enum so telemetry can aggregate by reason without leaking raw paths or agent/model names. +/// Network reach an adapter may use during discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnValidationErrorReason : IEquatable +public readonly struct ModelProviderDiscoveryNetworkScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistrySpawnValidationErrorReason(string value) + public ModelProviderDiscoveryNetworkScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Provided cwd does not exist on disk. - public static AgentRegistrySpawnValidationErrorReason CwdNotFound { get; } = new("cwd-not-found"); - - /// Provided cwd exists but is not a directory. - public static AgentRegistrySpawnValidationErrorReason CwdNotDirectory { get; } = new("cwd-not-directory"); + /// Discovery does not contact a network service. + public static ModelProviderDiscoveryNetworkScope None { get; } = new("none"); - /// Session name failed validateSessionName. - public static AgentRegistrySpawnValidationErrorReason InvalidName { get; } = new("invalid-name"); + /// Discovery is limited to loopback addresses on the local machine. + public static ModelProviderDiscoveryNetworkScope LoopbackOnly { get; } = new("loopbackOnly"); - /// Requested agent name was not found in builtin or custom agents. - public static AgentRegistrySpawnValidationErrorReason UnknownAgent { get; } = new("unknown-agent"); + /// Discovery contacts only endpoints the user already configured. + public static ModelProviderDiscoveryNetworkScope ConfiguredEndpointOnly { get; } = new("configuredEndpointOnly"); - /// Requested model is not available to this session. - public static AgentRegistrySpawnValidationErrorReason UnknownModel { get; } = new("unknown-model"); + /// Discovery may scan or contact the local network. + public static ModelProviderDiscoveryNetworkScope LocalNetwork { get; } = new("localNetwork"); - /// Caller asked for permissionMode='yolo' but the controller is not currently in allow-all mode. - public static AgentRegistrySpawnValidationErrorReason YoloNotAllowed { get; } = new("yolo-not-allowed"); + /// Discovery may contact remote internet services. + public static ModelProviderDiscoveryNetworkScope Internet { get; } = new("internet"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderDiscoveryNetworkScope left, ModelProviderDiscoveryNetworkScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderDiscoveryNetworkScope left, ModelProviderDiscoveryNetworkScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorReason other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderDiscoveryNetworkScope other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderDiscoveryNetworkScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31813,62 +34206,68 @@ public AgentRegistrySpawnValidationErrorReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistrySpawnValidationErrorReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderDiscoveryNetworkScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnValidationErrorReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderDiscoveryNetworkScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderDiscoveryNetworkScope)); } } } -/// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. +/// Kind of component that supplied a provider adapter or row. Attribution does not confer authority. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnPermissionMode : IEquatable +public readonly struct ModelProviderProvenanceSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AgentRegistrySpawnPermissionMode(string value) + public ModelProviderProvenanceSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Standard permission posture (prompts for each request). - public static AgentRegistrySpawnPermissionMode Default { get; } = new("default"); + /// Built into the runtime. + public static ModelProviderProvenanceSource BuiltIn { get; } = new("builtIn"); - /// Full allow-all (requires the controller-local session to currently be in allow-all mode). - public static AgentRegistrySpawnPermissionMode Yolo { get; } = new("yolo"); + /// Derived from existing user configuration. + public static ModelProviderProvenanceSource Configured { get; } = new("configured"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => left.Equals(right); + /// Supplied by an extension. + public static ModelProviderProvenanceSource Extension { get; } = new("extension"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode right) => !(left == right); + /// Supplied by another trusted contributor. + public static ModelProviderProvenanceSource Custom { get; } = new("custom"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderProvenanceSource left, ModelProviderProvenanceSource right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderProvenanceSource left, ModelProviderProvenanceSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnPermissionMode other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderProvenanceSource other && Equals(other); /// - public bool Equals(AgentRegistrySpawnPermissionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderProvenanceSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31876,65 +34275,62 @@ public AgentRegistrySpawnPermissionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AgentRegistrySpawnPermissionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderProvenanceSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AgentRegistrySpawnPermissionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderProvenanceSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnPermissionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderProvenanceSource)); } } } -/// How far OneAuth may go to acquire the requested token. +/// Transport to be used for provider requests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct EntraTokenInteraction : IEquatable +public readonly struct ProviderEndpointTransport : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public EntraTokenInteraction(string value) + public ProviderEndpointTransport(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Acquire the token without any user interaction, failing if interaction would be required. - public static EntraTokenInteraction Silent { get; } = new("silent"); - - /// Allow interactive acquisition, prompting the user only when a cached or silent token is unavailable. - public static EntraTokenInteraction Interactive { get; } = new("interactive"); + /// HTTP request/streaming transport. + public static ProviderEndpointTransport Http { get; } = new("http"); - /// Always prompt interactively, bypassing any cached or silently-refreshable token. - public static EntraTokenInteraction ForceInteractive { get; } = new("force-interactive"); + /// WebSocket transport. + public static ProviderEndpointTransport Websockets { get; } = new("websockets"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EntraTokenInteraction left, EntraTokenInteraction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderEndpointTransport left, ProviderEndpointTransport right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EntraTokenInteraction left, EntraTokenInteraction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderEndpointTransport left, ProviderEndpointTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is EntraTokenInteraction other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointTransport other && Equals(other); /// - public bool Equals(EntraTokenInteraction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderEndpointTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -31942,68 +34338,65 @@ public EntraTokenInteraction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override EntraTokenInteraction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderEndpointTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, EntraTokenInteraction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderEndpointTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EntraTokenInteraction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); } } } -/// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. +/// Provider family. Matches the `type` field of a BYOK provider config. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SendAgentMode : IEquatable +public readonly struct ProviderEndpointType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SendAgentMode(string value) + public ProviderEndpointType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The agent is responding interactively to the user. - public static SendAgentMode Interactive { get; } = new("interactive"); - - /// The agent is preparing a plan before making changes. - public static SendAgentMode Plan { get; } = new("plan"); + /// OpenAI-compatible endpoint (use the OpenAI client library). + public static ProviderEndpointType Openai { get; } = new("openai"); - /// The agent is working autonomously toward task completion. - public static SendAgentMode Autopilot { get; } = new("autopilot"); + /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). + public static ProviderEndpointType Azure { get; } = new("azure"); - /// The agent is in shell-focused UI mode. - public static SendAgentMode Shell { get; } = new("shell"); + /// Anthropic endpoint (use the Anthropic client library). + public static ProviderEndpointType Anthropic { get; } = new("anthropic"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SendAgentMode left, SendAgentMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderEndpointType left, ProviderEndpointType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SendAgentMode left, SendAgentMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderEndpointType left, ProviderEndpointType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointType other && Equals(other); /// - public bool Equals(SendAgentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderEndpointType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32011,62 +34404,62 @@ public SendAgentMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SendAgentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderEndpointType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SendAgentMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderEndpointType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); } } } -/// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. +/// Wire API to be used, when required for the provider type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SendMode : IEquatable +public readonly struct ProviderEndpointWireApi : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SendMode(string value) + public ProviderEndpointWireApi(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Append the message to the normal session queue. - public static SendMode Enqueue { get; } = new("enqueue"); + /// Classic chat-completions request shape. + public static ProviderEndpointWireApi Completions { get; } = new("completions"); - /// Interject the message during the in-progress turn. - public static SendMode Immediate { get; } = new("immediate"); + /// Newer responses request shape. + public static ProviderEndpointWireApi Responses { get; } = new("responses"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SendMode left, SendMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SendMode left, SendMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SendMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); /// - public bool Equals(SendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderEndpointWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32074,65 +34467,68 @@ public SendMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderEndpointWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SendMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderEndpointWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); } } } -/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". +/// Typed outcome for a provider operation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionLogLevel : IEquatable +public readonly struct ModelProviderOperationOutcomeCode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public SessionLogLevel(string value) + public ModelProviderOperationOutcomeCode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Informational message. - public static SessionLogLevel Info { get; } = new("info"); + /// The operation completed successfully; an empty inventory is valid. + public static ModelProviderOperationOutcomeCode Success { get; } = new("success"); - /// Warning message that may require attention. - public static SessionLogLevel Warning { get; } = new("warning"); + /// The provider or instance is absent during discovery, status, or model listing. Distinct from a successful empty inventory. + public static ModelProviderOperationOutcomeCode Absent { get; } = new("absent"); - /// Error message describing a failure. - public static SessionLogLevel Error { get; } = new("error"); + /// The provider is configured or expected but could not be reached. + public static ModelProviderOperationOutcomeCode Unreachable { get; } = new("unreachable"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionLogLevel left, SessionLogLevel right) => left.Equals(right); + /// The operation failed for a reason other than absence or reachability. + public static ModelProviderOperationOutcomeCode Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionLogLevel left, SessionLogLevel right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderOperationOutcomeCode left, ModelProviderOperationOutcomeCode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderOperationOutcomeCode left, ModelProviderOperationOutcomeCode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderOperationOutcomeCode other && Equals(other); /// - public bool Equals(SessionLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderOperationOutcomeCode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32140,65 +34536,59 @@ public SessionLogLevel(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override SessionLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderOperationOutcomeCode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, SessionLogLevel value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderOperationOutcomeCode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderOperationOutcomeCode)); } } } -/// Disposition of a permission request as observed by the responding client. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionDecisionOutcome : IEquatable +public readonly struct ProtocolAppendMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PermissionDecisionOutcome(string value) + public ProtocolAppendMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The request was approved automatically without a new human decision. - public static PermissionDecisionOutcome AutoApproved { get; } = new("auto_approved"); - - /// The request was denied without an interactive user decision; source records why. - public static PermissionDecisionOutcome AutopilotDenied { get; } = new("autopilot_denied"); - - /// The response came from an interactive user prompt. - public static PermissionDecisionOutcome PromptedUser { get; } = new("prompted_user"); + /// Gets the append value. + public static ProtocolAppendMode Append { get; } = new("append"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProtocolAppendMode left, ProtocolAppendMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolAppendMode left, ProtocolAppendMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionDecisionOutcome other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolAppendMode other && Equals(other); /// - public bool Equals(PermissionDecisionOutcome other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolAppendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32206,65 +34596,59 @@ public PermissionDecisionOutcome(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionDecisionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolAppendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionDecisionOutcome value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProtocolAppendMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionOutcome)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); } } } -/// Response capability available to the client when it settled a permission request. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionResponseCapability : IEquatable +public readonly struct ProtocolReplaceMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PermissionResponseCapability(string value) + public ProtocolReplaceMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// 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"); + /// Gets the replace value. + public static ProtocolReplaceMode Replace { get; } = new("replace"); - /// 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 equivalent. + public static bool operator ==(ProtocolReplaceMode left, ProtocolReplaceMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionResponseCapability left, PermissionResponseCapability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolReplaceMode left, ProtocolReplaceMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionResponseCapability other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolReplaceMode other && Equals(other); /// - public bool Equals(PermissionResponseCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolReplaceMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32272,71 +34656,59 @@ public PermissionResponseCapability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionResponseCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolReplaceMode 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) + public override void Write(Utf8JsonWriter writer, ProtocolReplaceMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionResponseCapability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); } } } -/// Client surface that submitted a permission response. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionDecisionSurface : IEquatable +public readonly struct ProtocolCustomizeMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PermissionDecisionSurface(string value) + public ProtocolCustomizeMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The interactive Copilot CLI terminal UI. - public static PermissionDecisionSurface Tui { get; } = new("tui"); - - /// The non-interactive Copilot CLI prompt mode. - public static PermissionDecisionSurface PromptMode { get; } = new("prompt_mode"); - - /// 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"); + /// Gets the customize value. + public static ProtocolCustomizeMode Customize { get; } = new("customize"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionDecisionSurface left, PermissionDecisionSurface right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionDecisionSurface left, PermissionDecisionSurface right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionDecisionSurface other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolCustomizeMode other && Equals(other); /// - public bool Equals(PermissionDecisionSurface other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolCustomizeMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32344,80 +34716,68 @@ public PermissionDecisionSurface(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionDecisionSurface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolCustomizeMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProtocolCustomizeMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionSurface)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); } } } -/// Authentication type. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AuthInfoType : IEquatable +public readonly struct ProtocolStaticSectionAction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AuthInfoType(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Authentication provided by a GitHub App HMAC credential. - public static AuthInfoType Hmac { get; } = new("hmac"); - - /// Authentication resolved from environment-provided credentials. - public static AuthInfoType Env { get; } = new("env"); - - /// Authentication from an interactive user sign-in. - public static AuthInfoType User { get; } = new("user"); + public ProtocolStaticSectionAction(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Authentication delegated to the GitHub CLI. - public static AuthInfoType GhCli { get; } = new("gh-cli"); + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Authentication from an API key credential. - public static AuthInfoType ApiKey { get; } = new("api-key"); + /// Replace the section content. + public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - /// Authentication from a GitHub token. - public static AuthInfoType Token { get; } = new("token"); + /// Remove the section content. + public static ProtocolStaticSectionAction Remove { get; } = new("remove"); - /// Authentication from an SDK GitHub token callback. - public static AuthInfoType TokenProvider { get; } = new("token-provider"); + /// Append content to the section. + public static ProtocolStaticSectionAction Append { get; } = new("append"); - /// Authentication from a Copilot API token. - public static AuthInfoType CopilotApiToken { get; } = new("copilot-api-token"); + /// Prepend content to the section. + public static ProtocolStaticSectionAction Prepend { get; } = new("prepend"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AuthInfoType left, AuthInfoType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AuthInfoType left, AuthInfoType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AuthInfoType other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); /// - public bool Equals(AuthInfoType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProtocolStaticSectionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32425,71 +34785,62 @@ public AuthInfoType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AuthInfoType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProtocolStaticSectionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProtocolStaticSectionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthInfoType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); } } } -/// The provider kind stamped on a signed-in account. +/// Whether a planned configuration entry is new or already present in the session registry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AccountKind : IEquatable +public readonly struct ModelProviderConfigurationDisposition : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AccountKind(string value) + public ModelProviderConfigurationDisposition(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// An OAuth github.com account. - public static AccountKind GitHubDotCom { get; } = new("githubDotCom"); - - /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com. - public static AccountKind Proxima { get; } = new("proxima"); - - /// A GitHub (EMU) account derived from a base Entra identity. - public static AccountKind EntraEmu { get; } = new("entraEmu"); - - /// A base Microsoft Entra identity. - public static AccountKind Entra { get; } = new("entra"); + /// No matching entry is registered; the caller should add the entry. + public static ModelProviderConfigurationDisposition Create { get; } = new("create"); - /// A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra identity as an EMU account; its bearer is a Loki-scoped inference token consumed through the model-provider path, not the GitHub switcher. - public static AccountKind Loki { get; } = new("loki"); + /// An equivalent entry is already registered; the caller should reuse it rather than adding a duplicate. + public static ModelProviderConfigurationDisposition AlreadyConfigured { get; } = new("alreadyConfigured"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AccountKind left, AccountKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderConfigurationDisposition left, ModelProviderConfigurationDisposition right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AccountKind left, AccountKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderConfigurationDisposition left, ModelProviderConfigurationDisposition right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AccountKind other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderConfigurationDisposition other && Equals(other); /// - public bool Equals(AccountKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelProviderConfigurationDisposition other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32497,65 +34848,77 @@ public AccountKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AccountKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelProviderConfigurationDisposition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AccountKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelProviderConfigurationDisposition value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AccountKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderConfigurationDisposition)); } } } -/// A provider a consumer may interactively sign in with. +/// The product serving the model, reported in telemetry as `model_provider`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct LoginProviderKind : IEquatable +public readonly struct ProviderConfigModelProvider : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public LoginProviderKind(string value) + public ProviderConfigModelProvider(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OAuth github.com sign-in via the browser (web loopback + PKCE). - public static LoginProviderKind GitHubDotCom { get; } = new("githubDotCom"); + /// OpenAI API. + public static ProviderConfigModelProvider Openai { get; } = new("openai"); - /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com; the host is supplied interactively through the neutral input-required step. - public static LoginProviderKind Proxima { get; } = new("proxima"); + /// Anthropic API. + public static ProviderConfigModelProvider Anthropic { get; } = new("anthropic"); - /// Microsoft Entra sign-in that derives a GitHub (EMU) credential. - public static LoginProviderKind Entra { get; } = new("entra"); + /// Azure OpenAI Service. + public static ProviderConfigModelProvider AzureOpenai { get; } = new("azure_openai"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(LoginProviderKind left, LoginProviderKind right) => left.Equals(right); + /// Ollama. + public static ProviderConfigModelProvider Ollama { get; } = new("ollama"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(LoginProviderKind left, LoginProviderKind right) => !(left == right); + /// LM Studio. + public static ProviderConfigModelProvider LmStudio { get; } = new("lm_studio"); + + /// Foundry Local. + public static ProviderConfigModelProvider FoundryLocal { get; } = new("foundry_local"); + + /// llama.cpp server. + public static ProviderConfigModelProvider LlamaCpp { get; } = new("llama_cpp"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigModelProvider left, ProviderConfigModelProvider right) => !(left == right); /// - public override bool Equals(object? obj) => obj is LoginProviderKind other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigModelProvider other && Equals(other); /// - public bool Equals(LoginProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigModelProvider other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32563,65 +34926,62 @@ public LoginProviderKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override LoginProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigModelProvider Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigModelProvider value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(LoginProviderKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigModelProvider)); } } } -/// Terminal disposition of a login persistence attempt. +/// Provider transport. Defaults to "http". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AuthLoginResultStatus : IEquatable +public readonly struct ProviderConfigTransport : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AuthLoginResultStatus(string value) + public ProviderConfigTransport(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The credential was persisted and the account is signed in. - public static AuthLoginResultStatus Completed { get; } = new("completed"); - - /// Persistence needs explicit consent to store the token in plaintext. - public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); + /// HTTP request/streaming transport. + public static ProviderConfigTransport Http { get; } = new("http"); - /// The user declined plaintext persistence. - public static AuthLoginResultStatus Declined { get; } = new("declined"); + /// WebSocket transport. + public static ProviderConfigTransport Websockets { get; } = new("websockets"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AuthLoginResultStatus left, AuthLoginResultStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigTransport left, ProviderConfigTransport right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AuthLoginResultStatus left, AuthLoginResultStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigTransport left, ProviderConfigTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AuthLoginResultStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigTransport other && Equals(other); /// - public bool Equals(AuthLoginResultStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32629,68 +34989,65 @@ public AuthLoginResultStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AuthLoginResultStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AuthLoginResultStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthLoginResultStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); } } } -/// Source category for a collected debug bundle entry. +/// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsSource : IEquatable +public readonly struct ProviderConfigType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsSource(string value) + public ProviderConfigType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Session event log. - public static DebugCollectLogsSource Events { get; } = new("events"); - - /// Process log for the session. - public static DebugCollectLogsSource ProcessLog { get; } = new("process-log"); + /// Generic OpenAI-compatible API. + public static ProviderConfigType Openai { get; } = new("openai"); - /// Interactive shell log for the session. - public static DebugCollectLogsSource ShellLog { get; } = new("shell-log"); + /// Azure OpenAI Service endpoint. + public static ProviderConfigType Azure { get; } = new("azure"); - /// Caller-provided diagnostic entry. - public static DebugCollectLogsSource Additional { get; } = new("additional"); + /// Anthropic API endpoint. + public static ProviderConfigType Anthropic { get; } = new("anthropic"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsSource left, DebugCollectLogsSource right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigType left, ProviderConfigType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsSource left, DebugCollectLogsSource right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsSource other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigType other && Equals(other); /// - public bool Equals(DebugCollectLogsSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32698,62 +35055,62 @@ public DebugCollectLogsSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); } } } -/// Destination kind that was written. +/// Wire API format (openai/azure only). Defaults to "completions". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsResultKind : IEquatable +public readonly struct ProviderConfigWireApi : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsResultKind(string value) + public ProviderConfigWireApi(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A .tgz archive was written. - public static DebugCollectLogsResultKind Archive { get; } = new("archive"); + /// OpenAI Chat Completions wire format. + public static ProviderConfigWireApi Completions { get; } = new("completions"); - /// A directory containing the collected files was written. - public static DebugCollectLogsResultKind Directory { get; } = new("directory"); + /// OpenAI Responses API wire format. + public static ProviderConfigWireApi Responses { get; } = new("responses"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ProviderConfigWireApi left, ProviderConfigWireApi right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ProviderConfigWireApi left, ProviderConfigWireApi right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsResultKind other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigWireApi other && Equals(other); /// - public bool Equals(DebugCollectLogsResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ProviderConfigWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32761,62 +35118,65 @@ public DebugCollectLogsResultKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProviderConfigWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsResultKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProviderConfigWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsResultKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigWireApi)); } } } -/// Kind of caller-provided debug log entry. +/// Disposition of a permission request as observed by the responding client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsEntryKind : IEquatable +public readonly struct PermissionDecisionOutcome : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsEntryKind(string value) + public PermissionDecisionOutcome(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Include a single server-local file. - public static DebugCollectLogsEntryKind File { get; } = new("file"); + /// The request was approved automatically without a new human decision. + public static PermissionDecisionOutcome AutoApproved { get; } = new("auto_approved"); - /// Include files from a server-local directory recursively. - public static DebugCollectLogsEntryKind Directory { get; } = new("directory"); + /// The request was denied without an interactive user decision; source records why. + public static PermissionDecisionOutcome AutopilotDenied { get; } = new("autopilot_denied"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => left.Equals(right); + /// The response came from an interactive user prompt. + public static PermissionDecisionOutcome PromptedUser { get; } = new("prompted_user"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionDecisionOutcome left, PermissionDecisionOutcome right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DebugCollectLogsEntryKind other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionDecisionOutcome other && Equals(other); /// - public bool Equals(DebugCollectLogsEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionDecisionOutcome other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32824,65 +35184,65 @@ public DebugCollectLogsEntryKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionDecisionOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsEntryKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionDecisionOutcome value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsEntryKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionOutcome)); } } } -/// How a collected debug entry should be redacted before being staged. +/// Response capability available to the client when it settled a permission request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DebugCollectLogsRedaction : IEquatable +public readonly struct PermissionResponseCapability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DebugCollectLogsRedaction(string value) + public PermissionResponseCapability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Redact the file as plain UTF-8 log text. - public static DebugCollectLogsRedaction PlainText { get; } = new("plain-text"); + /// The client could ask a user for this decision. + public static PermissionResponseCapability Interactive { get; } = new("interactive"); - /// Redact each non-empty line as a session event JSON object, falling back to plain-text redaction for malformed lines. - public static DebugCollectLogsRedaction EventsJsonl { get; } = new("events-jsonl"); + /// The client could return an automated response but could not ask a user. + public static PermissionResponseCapability Headless { get; } = new("headless"); - /// No redaction is applied. The caller must ensure any necessary redaction is performed before this call. - public static DebugCollectLogsRedaction None { get; } = new("none"); + /// 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 ==(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => left.Equals(right); + /// 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 !=(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => !(left == 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 DebugCollectLogsRedaction other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionResponseCapability other && Equals(other); /// - public bool Equals(DebugCollectLogsRedaction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionResponseCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32890,65 +35250,71 @@ public DebugCollectLogsRedaction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DebugCollectLogsRedaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionResponseCapability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DebugCollectLogsRedaction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionResponseCapability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsRedaction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionResponseCapability)); } } } -/// Cumulative resource ceiling that stopped a workflow run. +/// Client surface that submitted a permission response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowRunFailureKind : IEquatable +public readonly struct PermissionDecisionSurface : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowRunFailureKind(string value) + public PermissionDecisionSurface(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The run admitted the approved maximum total number of subagents. - public static WorkflowRunFailureKind MaxTotalSubagents { get; } = new("maxTotalSubagents"); + /// The interactive Copilot CLI terminal UI. + public static PermissionDecisionSurface Tui { get; } = new("tui"); - /// The run reached the approved accumulated active-execution time in seconds. - public static WorkflowRunFailureKind TimeoutSeconds { get; } = new("timeoutSeconds"); + /// The non-interactive Copilot CLI prompt mode. + public static PermissionDecisionSurface PromptMode { get; } = new("prompt_mode"); - /// The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. - public static WorkflowRunFailureKind MaxAiCredits { get; } = new("maxAiCredits"); + /// The Copilot App client. + public static PermissionDecisionSurface CopilotApp { get; } = new("copilot_app"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => left.Equals(right); + /// An Agent Client Protocol host. + public static PermissionDecisionSurface Acp { get; } = new("acp"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => !(left == right); + /// A generic Copilot SDK client. + public static PermissionDecisionSurface Sdk { get; } = new("sdk"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionDecisionSurface left, PermissionDecisionSurface right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionDecisionSurface left, PermissionDecisionSurface right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowRunFailureKind other && Equals(other); + public override bool Equals(object? obj) => obj is PermissionDecisionSurface other && Equals(other); /// - public bool Equals(WorkflowRunFailureKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(PermissionDecisionSurface other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -32956,89 +35322,71 @@ public WorkflowRunFailureKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowRunFailureKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override PermissionDecisionSurface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowRunFailureKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PermissionDecisionSurface value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunFailureKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionDecisionSurface)); } } } -/// Execution-critical workflow storage operation. +/// The provider kind stamped on a signed-in account. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowDurableOperation : IEquatable +public readonly struct AccountKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowDurableOperation(string value) + public AccountKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Creating the durable run and declared phases. - public static WorkflowDurableOperation CreateRun { get; } = new("createRun"); - - /// Persisting the transition to running. - public static WorkflowDurableOperation MarkRunStarted { get; } = new("markRunStarted"); - - /// Persisting the terminal run envelope. - public static WorkflowDurableOperation FinishRun { get; } = new("finishRun"); - - /// Persisting subagent admission accounting. - public static WorkflowDurableOperation ReserveAgent { get; } = new("reserveAgent"); - - /// Rolling back an uncommitted subagent admission. - public static WorkflowDurableOperation ReleaseAgent { get; } = new("releaseAgent"); - - /// Persisting an idempotent model-usage charge. - public static WorkflowDurableOperation ChargeCredit { get; } = new("chargeCredit"); - - /// Persisting active execution time. - public static WorkflowDurableOperation AddElapsed { get; } = new("addElapsed"); + /// An OAuth github.com account. + public static AccountKind GitHubDotCom { get; } = new("githubDotCom"); - /// Reading the authoritative AI-credit total. - public static WorkflowDurableOperation ReconcileCreditTotal { get; } = new("reconcileCreditTotal"); + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com. + public static AccountKind Proxima { get; } = new("proxima"); - /// Reading a journal entry without treating storage failure as a cache miss. - public static WorkflowDurableOperation JournalGet { get; } = new("journalGet"); + /// A GitHub (EMU) account derived from a base Entra identity. + public static AccountKind EntraEmu { get; } = new("entraEmu"); - /// Persisting a journal entry before reporting success. - public static WorkflowDurableOperation JournalPut { get; } = new("journalPut"); + /// A base Microsoft Entra identity. + public static AccountKind Entra { get; } = new("entra"); - /// Renewing the durable owner lease that proves this process still owns the run. - public static WorkflowDurableOperation RefreshLease { get; } = new("refreshLease"); + /// A Microsoft 365 Copilot (Loki) inference account derived from the same base Entra identity as an EMU account; its bearer is a Loki-scoped inference token consumed through the model-provider path, not the GitHub switcher. + public static AccountKind Loki { get; } = new("loki"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowDurableOperation left, WorkflowDurableOperation right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AccountKind left, AccountKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowDurableOperation left, WorkflowDurableOperation right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AccountKind left, AccountKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowDurableOperation other && Equals(other); + public override bool Equals(object? obj) => obj is AccountKind other && Equals(other); /// - public bool Equals(WorkflowDurableOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AccountKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33046,77 +35394,65 @@ public WorkflowDurableOperation(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowDurableOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AccountKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowDurableOperation value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AccountKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowDurableOperation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AccountKind)); } } } -/// Current or terminal state of a workflow run. +/// A provider a consumer may interactively sign in with. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowRunStatus : IEquatable +public readonly struct LoginProviderKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowRunStatus(string value) + public LoginProviderKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The run was minted and is awaiting approval. - public static WorkflowRunStatus Pending { get; } = new("pending"); - - /// The run is executing. - public static WorkflowRunStatus Running { get; } = new("running"); - - /// The run completed successfully. - public static WorkflowRunStatus Completed { get; } = new("completed"); - - /// The run was interrupted while resource budget remained. - public static WorkflowRunStatus Halted { get; } = new("halted"); - - /// The current attempt stopped intentionally and the run may be resumed. - public static WorkflowRunStatus Paused { get; } = new("paused"); + /// OAuth github.com sign-in via the browser (web loopback + PKCE). + public static LoginProviderKind GitHubDotCom { get; } = new("githubDotCom"); - /// The run was cancelled before completion. - public static WorkflowRunStatus Cancelled { get; } = new("cancelled"); + /// A GitHub Enterprise Cloud account — a GitHub account on a non-github.com host, e.g. *.ghe.com; the host is supplied interactively through the neutral input-required step. + public static LoginProviderKind Proxima { get; } = new("proxima"); - /// The workflow body failed or reached a cumulative resource ceiling. - public static WorkflowRunStatus Error { get; } = new("error"); + /// Microsoft Entra sign-in that derives a GitHub (EMU) credential. + public static LoginProviderKind Entra { get; } = new("entra"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowRunStatus left, WorkflowRunStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(LoginProviderKind left, LoginProviderKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowRunStatus left, WorkflowRunStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(LoginProviderKind left, LoginProviderKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowRunStatus other && Equals(other); + public override bool Equals(object? obj) => obj is LoginProviderKind other && Equals(other); /// - public bool Equals(WorkflowRunStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(LoginProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33124,68 +35460,68 @@ public WorkflowRunStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowRunStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override LoginProviderKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowRunStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(LoginProviderKind)); } } } -/// Derived lifecycle state of a workflow phase. +/// Disposition of a login attempt, including pending user decisions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowPhaseStatus : IEquatable +public readonly struct AuthLoginResultStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowPhaseStatus(string value) + public AuthLoginResultStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The phase has not been entered yet. - public static WorkflowPhaseStatus Pending { get; } = new("pending"); + /// The credential was persisted and the selected account is signed in. + public static AuthLoginResultStatus Completed { get; } = new("completed"); - /// The phase is currently entered and accumulating active time. - public static WorkflowPhaseStatus Active { get; } = new("active"); + /// Persistence needs explicit consent to store the token in plaintext. + public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); - /// The phase was entered and has since been closed. - public static WorkflowPhaseStatus Completed { get; } = new("completed"); + /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. + public static AuthLoginResultStatus NeedsAccountSelection { get; } = new("needs-account-selection"); - /// The phase was never entered because a later phase was entered or the run reached a terminal state. - public static WorkflowPhaseStatus Skipped { get; } = new("skipped"); + /// The user declined plaintext persistence. + public static AuthLoginResultStatus Declined { get; } = new("declined"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AuthLoginResultStatus left, AuthLoginResultStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AuthLoginResultStatus left, AuthLoginResultStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowPhaseStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AuthLoginResultStatus other && Equals(other); /// - public bool Equals(WorkflowPhaseStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AuthLoginResultStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33193,62 +35529,68 @@ public WorkflowPhaseStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowPhaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AuthLoginResultStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowPhaseStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AuthLoginResultStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPhaseStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AuthLoginResultStatus)); } } } -/// Kind of workflow progress line. +/// Source category for a collected debug bundle entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowLogLineKind : IEquatable +public readonly struct DebugCollectLogsSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowLogLineKind(string value) + public DebugCollectLogsSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A narrator log line. - public static WorkflowLogLineKind Log { get; } = new("log"); + /// Session event log. + public static DebugCollectLogsSource Events { get; } = new("events"); - /// A named workflow phase marker. - public static WorkflowLogLineKind Phase { get; } = new("phase"); + /// Process log for the session. + public static DebugCollectLogsSource ProcessLog { get; } = new("process-log"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowLogLineKind left, WorkflowLogLineKind right) => left.Equals(right); + /// Interactive shell log for the session. + public static DebugCollectLogsSource ShellLog { get; } = new("shell-log"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowLogLineKind left, WorkflowLogLineKind right) => !(left == right); + /// Caller-provided diagnostic entry. + public static DebugCollectLogsSource Additional { get; } = new("additional"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsSource left, DebugCollectLogsSource right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsSource left, DebugCollectLogsSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowLogLineKind other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsSource other && Equals(other); /// - public bool Equals(WorkflowLogLineKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33256,62 +35598,62 @@ public WorkflowLogLineKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowLogLineKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowLogLineKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowLogLineKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsSource)); } } } -/// Action the runtime selected for a durable workflow pause checkpoint. +/// Destination kind that was written. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowPauseCheckpointAction : IEquatable +public readonly struct DebugCollectLogsResultKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkflowPauseCheckpointAction(string value) + public DebugCollectLogsResultKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The checkpoint was committed by a prior paused attempt, so execution may continue. - public static WorkflowPauseCheckpointAction Continue { get; } = new("continue"); + /// A .tgz archive was written. + public static DebugCollectLogsResultKind Archive { get; } = new("archive"); - /// This attempt claimed the checkpoint and must cooperatively stop. - public static WorkflowPauseCheckpointAction Pause { get; } = new("pause"); + /// A directory containing the collected files was written. + public static DebugCollectLogsResultKind Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsResultKind left, DebugCollectLogsResultKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkflowPauseCheckpointAction other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsResultKind other && Equals(other); /// - public bool Equals(WorkflowPauseCheckpointAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33319,62 +35661,62 @@ public WorkflowPauseCheckpointAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkflowPauseCheckpointAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkflowPauseCheckpointAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsResultKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPauseCheckpointAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsResultKind)); } } } -/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// Kind of caller-provided debug log entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelSwitchAutoTierStatus : IEquatable +public readonly struct DebugCollectLogsEntryKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ModelSwitchAutoTierStatus(string value) + public DebugCollectLogsEntryKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. - public static ModelSwitchAutoTierStatus Unchanged { get; } = new("unchanged"); + /// Include a single server-local file. + public static DebugCollectLogsEntryKind File { get; } = new("file"); - /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. - public static ModelSwitchAutoTierStatus Pending { get; } = new("pending"); + /// Include files from a server-local directory recursively. + public static DebugCollectLogsEntryKind Directory { get; } = new("directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsEntryKind left, DebugCollectLogsEntryKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ModelSwitchAutoTierStatus other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsEntryKind other && Equals(other); /// - public bool Equals(ModelSwitchAutoTierStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsEntryKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33382,62 +35724,65 @@ public ModelSwitchAutoTierStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ModelSwitchAutoTierStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsEntryKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ModelSwitchAutoTierStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsEntryKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsEntryKind)); } } } -/// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. +/// How a collected debug entry should be redacted before being staged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspacesWorkspaceDetailsHostType : IEquatable +public readonly struct DebugCollectLogsRedaction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkspacesWorkspaceDetailsHostType(string value) + public DebugCollectLogsRedaction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Workspace repository is hosted on GitHub. - public static WorkspacesWorkspaceDetailsHostType GitHub { get; } = new("github"); + /// Redact the file as plain UTF-8 log text. + public static DebugCollectLogsRedaction PlainText { get; } = new("plain-text"); - /// Workspace repository is hosted on Azure DevOps. - public static WorkspacesWorkspaceDetailsHostType Ado { get; } = new("ado"); + /// Redact each non-empty line as a session event JSON object, falling back to plain-text redaction for malformed lines. + public static DebugCollectLogsRedaction EventsJsonl { get; } = new("events-jsonl"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => left.Equals(right); + /// No redaction is applied. The caller must ensure any necessary redaction is performed before this call. + public static DebugCollectLogsRedaction None { get; } = new("none"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DebugCollectLogsRedaction left, DebugCollectLogsRedaction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkspacesWorkspaceDetailsHostType other && Equals(other); + public override bool Equals(object? obj) => obj is DebugCollectLogsRedaction other && Equals(other); /// - public bool Equals(WorkspacesWorkspaceDetailsHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DebugCollectLogsRedaction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33445,68 +35790,65 @@ public WorkspacesWorkspaceDetailsHostType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkspacesWorkspaceDetailsHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DebugCollectLogsRedaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkspacesWorkspaceDetailsHostType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DebugCollectLogsRedaction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspacesWorkspaceDetailsHostType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DebugCollectLogsRedaction)); } } } -/// Type of change represented by this file diff. +/// Cumulative resource ceiling that stopped a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspaceDiffFileChangeType : IEquatable +public readonly struct WorkflowRunFailureKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkspaceDiffFileChangeType(string value) + public WorkflowRunFailureKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The file was added. - public static WorkspaceDiffFileChangeType Added { get; } = new("added"); - - /// The file was modified. - public static WorkspaceDiffFileChangeType Modified { get; } = new("modified"); + /// The run admitted the approved maximum total number of subagents. + public static WorkflowRunFailureKind MaxTotalSubagents { get; } = new("maxTotalSubagents"); - /// The file was deleted. - public static WorkspaceDiffFileChangeType Deleted { get; } = new("deleted"); + /// The run reached the approved accumulated active-execution time in seconds. + public static WorkflowRunFailureKind TimeoutSeconds { get; } = new("timeoutSeconds"); - /// The file was renamed. - public static WorkspaceDiffFileChangeType Renamed { get; } = new("renamed"); + /// The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. + public static WorkflowRunFailureKind MaxAiCredits { get; } = new("maxAiCredits"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowRunFailureKind left, WorkflowRunFailureKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkspaceDiffFileChangeType other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowRunFailureKind other && Equals(other); /// - public bool Equals(WorkspaceDiffFileChangeType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowRunFailureKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33514,65 +35856,89 @@ public WorkspaceDiffFileChangeType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkspaceDiffFileChangeType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowRunFailureKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkspaceDiffFileChangeType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowRunFailureKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffFileChangeType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunFailureKind)); } } } -/// Diff mode requested by the client. +/// Execution-critical workflow storage operation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspaceDiffMode : IEquatable +public readonly struct WorkflowDurableOperation : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public WorkspaceDiffMode(string value) + public WorkflowDurableOperation(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Return staged, unstaged, and untracked working tree changes. - public static WorkspaceDiffMode Unstaged { get; } = new("unstaged"); + /// Creating the durable run and declared phases. + public static WorkflowDurableOperation CreateRun { get; } = new("createRun"); - /// Return changes compared with the default branch. - public static WorkspaceDiffMode Branch { get; } = new("branch"); + /// Persisting the transition to running. + public static WorkflowDurableOperation MarkRunStarted { get; } = new("markRunStarted"); - /// Return the cumulative diff of files Copilot changed this session (used in non-git workspaces). - public static WorkspaceDiffMode Session { get; } = new("session"); + /// Persisting the terminal run envelope. + public static WorkflowDurableOperation FinishRun { get; } = new("finishRun"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(WorkspaceDiffMode left, WorkspaceDiffMode right) => left.Equals(right); + /// Persisting subagent admission accounting. + public static WorkflowDurableOperation ReserveAgent { get; } = new("reserveAgent"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(WorkspaceDiffMode left, WorkspaceDiffMode right) => !(left == right); + /// Rolling back an uncommitted subagent admission. + public static WorkflowDurableOperation ReleaseAgent { get; } = new("releaseAgent"); + + /// Persisting an idempotent model-usage charge. + public static WorkflowDurableOperation ChargeCredit { get; } = new("chargeCredit"); + + /// Persisting active execution time. + public static WorkflowDurableOperation AddElapsed { get; } = new("addElapsed"); + + /// Reading the authoritative AI-credit total. + public static WorkflowDurableOperation ReconcileCreditTotal { get; } = new("reconcileCreditTotal"); + + /// Reading a journal entry without treating storage failure as a cache miss. + public static WorkflowDurableOperation JournalGet { get; } = new("journalGet"); + + /// Persisting a journal entry before reporting success. + public static WorkflowDurableOperation JournalPut { get; } = new("journalPut"); + + /// Renewing the durable owner lease that proves this process still owns the run. + public static WorkflowDurableOperation RefreshLease { get; } = new("refreshLease"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowDurableOperation left, WorkflowDurableOperation right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowDurableOperation left, WorkflowDurableOperation right) => !(left == right); /// - public override bool Equals(object? obj) => obj is WorkspaceDiffMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowDurableOperation other && Equals(other); /// - public bool Equals(WorkspaceDiffMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowDurableOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33580,65 +35946,77 @@ public WorkspaceDiffMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override WorkspaceDiffMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowDurableOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, WorkspaceDiffMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowDurableOperation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowDurableOperation)); } } } -/// Reason a rewind read (rewind points, file-restore preview, or session diff) could not be answered from the session's file-change captures. +/// Current or terminal state of a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HistoryRewindUnavailableReason : IEquatable +public readonly struct WorkflowRunStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public HistoryRewindUnavailableReason(string value) + public WorkflowRunStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The session did not opt into file-change tracking before its first turn. - public static HistoryRewindUnavailableReason FileChangeTrackingDisabled { get; } = new("file-change-tracking-disabled"); + /// The run was minted and is awaiting approval. + public static WorkflowRunStatus Pending { get; } = new("pending"); - /// The session still has work that may mutate files or history. Transient: the same request succeeds once the session settles, so callers should retry rather than treat it as a failure. - public static HistoryRewindUnavailableReason SessionBusy { get; } = new("session-busy"); + /// The run is executing. + public static WorkflowRunStatus Running { get; } = new("running"); - /// Remote-backed rewind routing is not supported. - public static HistoryRewindUnavailableReason UnsupportedRemoteSession { get; } = new("unsupported-remote-session"); + /// The run completed successfully. + public static WorkflowRunStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => left.Equals(right); + /// The run was interrupted while resource budget remained. + public static WorkflowRunStatus Halted { get; } = new("halted"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => !(left == right); + /// The current attempt stopped intentionally and the run may be resumed. + public static WorkflowRunStatus Paused { get; } = new("paused"); + + /// The run was cancelled before completion. + public static WorkflowRunStatus Cancelled { get; } = new("cancelled"); + + /// The workflow body failed or reached a cumulative resource ceiling. + public static WorkflowRunStatus Error { get; } = new("error"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowRunStatus left, WorkflowRunStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowRunStatus left, WorkflowRunStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is HistoryRewindUnavailableReason other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowRunStatus other && Equals(other); /// - public bool Equals(HistoryRewindUnavailableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowRunStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33646,65 +36024,68 @@ public HistoryRewindUnavailableReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override HistoryRewindUnavailableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowRunStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, HistoryRewindUnavailableReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowRunStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HistoryRewindUnavailableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunStatus)); } } } -/// Current normalized autopilot objective lifecycle status. +/// Derived lifecycle state of a workflow phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AutopilotObjectiveStatus : IEquatable +public readonly struct WorkflowPhaseStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public AutopilotObjectiveStatus(string value) + public WorkflowPhaseStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The objective is actively running. - public static AutopilotObjectiveStatus Active { get; } = new("active"); + /// The phase has not been entered yet. + public static WorkflowPhaseStatus Pending { get; } = new("pending"); - /// The objective is paused and may be resumed. - public static AutopilotObjectiveStatus Paused { get; } = new("paused"); + /// The phase is currently entered and accumulating active time. + public static WorkflowPhaseStatus Active { get; } = new("active"); - /// The objective completed. - public static AutopilotObjectiveStatus Completed { get; } = new("completed"); + /// The phase was entered and has since been closed. + public static WorkflowPhaseStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => left.Equals(right); + /// The phase was never entered because a later phase was entered or the run reached a terminal state. + public static WorkflowPhaseStatus Skipped { get; } = new("skipped"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowPhaseStatus left, WorkflowPhaseStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowPhaseStatus other && Equals(other); /// - public bool Equals(AutopilotObjectiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowPhaseStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33712,62 +36093,62 @@ public AutopilotObjectiveStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override AutopilotObjectiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowPhaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, AutopilotObjectiveStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowPhaseStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPhaseStatus)); } } } -/// Whether task execution is synchronously awaited or managed in the background. +/// Kind of workflow progress line. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskExecutionMode : IEquatable +public readonly struct WorkflowLogLineKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskExecutionMode(string value) + public WorkflowLogLineKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The task was started with synchronous waiting. - public static TaskExecutionMode Sync { get; } = new("sync"); + /// A narrator log line. + public static WorkflowLogLineKind Log { get; } = new("log"); - /// The task is managed in the background. - public static TaskExecutionMode Background { get; } = new("background"); + /// A named workflow phase marker. + public static WorkflowLogLineKind Phase { get; } = new("phase"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskExecutionMode left, TaskExecutionMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowLogLineKind left, WorkflowLogLineKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskExecutionMode left, TaskExecutionMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowLogLineKind left, WorkflowLogLineKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskExecutionMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowLogLineKind other && Equals(other); /// - public bool Equals(TaskExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowLogLineKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33775,71 +36156,62 @@ public TaskExecutionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowLogLineKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskExecutionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowLogLineKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskExecutionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowLogLineKind)); } } } -/// Current lifecycle status of the task. +/// Action the runtime selected for a durable workflow pause checkpoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskStatus : IEquatable +public readonly struct WorkflowPauseCheckpointAction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public TaskStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The task is actively executing. - public static TaskStatus Running { get; } = new("running"); - - /// The task is waiting for additional input. - public static TaskStatus Idle { get; } = new("idle"); + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public WorkflowPauseCheckpointAction(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// The task finished successfully. - public static TaskStatus Completed { get; } = new("completed"); + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// The task finished with an error. - public static TaskStatus Failed { get; } = new("failed"); + /// The checkpoint was committed by a prior paused attempt, so execution may continue. + public static WorkflowPauseCheckpointAction Continue { get; } = new("continue"); - /// The task was cancelled before completion. - public static TaskStatus Cancelled { get; } = new("cancelled"); + /// This attempt claimed the checkpoint and must cooperatively stop. + public static WorkflowPauseCheckpointAction Pause { get; } = new("pause"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskStatus left, TaskStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskStatus left, TaskStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowPauseCheckpointAction other && Equals(other); /// - public bool Equals(TaskStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkflowPauseCheckpointAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33847,59 +36219,62 @@ public TaskStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkflowPauseCheckpointAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkflowPauseCheckpointAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPauseCheckpointAction)); } } } -/// Client-owned tasks always execute outside the runtime in background mode. +/// Whether the requested preference was already effective or was accepted for later transactional activation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientExecutionMode : IEquatable +public readonly struct ModelSwitchAutoTierStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientExecutionMode(string value) + public ModelSwitchAutoTierStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the background value. - public static TaskClientExecutionMode Background { get; } = new("background"); + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + public static ModelSwitchAutoTierStatus Unchanged { get; } = new("unchanged"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientExecutionMode left, TaskClientExecutionMode right) => left.Equals(right); + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + public static ModelSwitchAutoTierStatus Pending { get; } = new("pending"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientExecutionMode left, TaskClientExecutionMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientExecutionMode other && Equals(other); + public override bool Equals(object? obj) => obj is ModelSwitchAutoTierStatus other && Equals(other); /// - public bool Equals(TaskClientExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ModelSwitchAutoTierStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33907,62 +36282,62 @@ public TaskClientExecutionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ModelSwitchAutoTierStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientExecutionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ModelSwitchAutoTierStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); } } } -/// Connection class owning a client task. +/// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientOwnerKind : IEquatable +public readonly struct WorkspacesWorkspaceDetailsHostType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientOwnerKind(string value) + public WorkspacesWorkspaceDetailsHostType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A discovered extension connection owns the task. - public static TaskClientOwnerKind Extension { get; } = new("extension"); + /// Workspace repository is hosted on GitHub. + public static WorkspacesWorkspaceDetailsHostType GitHub { get; } = new("github"); - /// A generic SDK connection owns the task. - public static TaskClientOwnerKind Sdk { get; } = new("sdk"); + /// Workspace repository is hosted on Azure DevOps. + public static WorkspacesWorkspaceDetailsHostType Ado { get; } = new("ado"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientOwnerKind left, TaskClientOwnerKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspacesWorkspaceDetailsHostType other && Equals(other); /// - public bool Equals(TaskClientOwnerKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkspacesWorkspaceDetailsHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -33970,62 +36345,68 @@ public TaskClientOwnerKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientOwnerKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkspacesWorkspaceDetailsHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientOwnerKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkspacesWorkspaceDetailsHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspacesWorkspaceDetailsHostType)); } } } -/// Presence of the task's bound join. +/// Type of change represented by this file diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientOwnerPresence : IEquatable +public readonly struct WorkspaceDiffFileChangeType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientOwnerPresence(string value) + public WorkspaceDiffFileChangeType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The bound session join is connected. - public static TaskClientOwnerPresence Connected { get; } = new("connected"); + /// The file was added. + public static WorkspaceDiffFileChangeType Added { get; } = new("added"); - /// The bound session join is disconnected. - public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); + /// The file was modified. + public static WorkspaceDiffFileChangeType Modified { get; } = new("modified"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); + /// The file was deleted. + public static WorkspaceDiffFileChangeType Deleted { get; } = new("deleted"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == right); + /// The file was renamed. + public static WorkspaceDiffFileChangeType Renamed { get; } = new("renamed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkspaceDiffFileChangeType left, WorkspaceDiffFileChangeType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspaceDiffFileChangeType other && Equals(other); /// - public bool Equals(TaskClientOwnerPresence other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkspaceDiffFileChangeType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34033,74 +36414,65 @@ public TaskClientOwnerPresence(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientOwnerPresence Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkspaceDiffFileChangeType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientOwnerPresence value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkspaceDiffFileChangeType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffFileChangeType)); } } } -/// Lifecycle status of a client-owned task. +/// Diff mode requested by the client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientStatus : IEquatable +public readonly struct WorkspaceDiffMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientStatus(string value) + public WorkspaceDiffMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The external owner is actively working. - public static TaskClientStatus Running { get; } = new("running"); - - /// The external owner is connected but waiting. - public static TaskClientStatus Idle { get; } = new("idle"); - - /// The owner reported successful completion. - public static TaskClientStatus Completed { get; } = new("completed"); - - /// The owner reported failure. - public static TaskClientStatus Failed { get; } = new("failed"); + /// Return staged, unstaged, and untracked working tree changes. + public static WorkspaceDiffMode Unstaged { get; } = new("unstaged"); - /// The owner reported or confirmed cancellation. - public static TaskClientStatus Cancelled { get; } = new("cancelled"); + /// Return changes compared with the default branch. + public static WorkspaceDiffMode Branch { get; } = new("branch"); - /// The bound owner join disappeared; external executor state is unknown. - public static TaskClientStatus Orphaned { get; } = new("orphaned"); + /// Return the cumulative diff of files Copilot changed this session (used in non-git workspaces). + public static WorkspaceDiffMode Session { get; } = new("session"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientStatus left, TaskClientStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkspaceDiffMode left, WorkspaceDiffMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientStatus left, TaskClientStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkspaceDiffMode left, WorkspaceDiffMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspaceDiffMode other && Equals(other); /// - public bool Equals(TaskClientStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(WorkspaceDiffMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34108,62 +36480,65 @@ public TaskClientStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override WorkspaceDiffMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, WorkspaceDiffMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffMode)); } } } -/// Whether the shell runs inside a managed PTY session or as an independent background process. +/// Reason a rewind read (rewind points, file-restore preview, or session diff) could not be answered from the session's file-change captures. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskShellInfoAttachmentMode : IEquatable +public readonly struct HistoryRewindUnavailableReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskShellInfoAttachmentMode(string value) + public HistoryRewindUnavailableReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The shell runs in a managed PTY session. - public static TaskShellInfoAttachmentMode Attached { get; } = new("attached"); + /// The session did not opt into file-change tracking before its first turn. + public static HistoryRewindUnavailableReason FileChangeTrackingDisabled { get; } = new("file-change-tracking-disabled"); - /// The shell runs as an independent background process. - public static TaskShellInfoAttachmentMode Detached { get; } = new("detached"); + /// The session still has work that may mutate files or history. Transient: the same request succeeds once the session settles, so callers should retry rather than treat it as a failure. + public static HistoryRewindUnavailableReason SessionBusy { get; } = new("session-busy"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => left.Equals(right); + /// Remote-backed rewind routing is not supported. + public static HistoryRewindUnavailableReason UnsupportedRemoteSession { get; } = new("unsupported-remote-session"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskShellInfoAttachmentMode other && Equals(other); + public override bool Equals(object? obj) => obj is HistoryRewindUnavailableReason other && Equals(other); /// - public bool Equals(TaskShellInfoAttachmentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(HistoryRewindUnavailableReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34171,59 +36546,65 @@ public TaskShellInfoAttachmentMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskShellInfoAttachmentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override HistoryRewindUnavailableReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, HistoryRewindUnavailableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskShellInfoAttachmentMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HistoryRewindUnavailableReason)); } } } -/// Discriminator for a client-owned task. +/// Current normalized autopilot objective lifecycle status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientType : IEquatable +public readonly struct AutopilotObjectiveStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientType(string value) + public AutopilotObjectiveStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the client value. - public static TaskClientType Client { get; } = new("client"); + /// The objective is actively running. + public static AutopilotObjectiveStatus Active { get; } = new("active"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); + /// The objective is paused and may be resumed. + public static AutopilotObjectiveStatus Paused { get; } = new("paused"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == right); + /// The objective completed. + public static AutopilotObjectiveStatus Completed { get; } = new("completed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); + public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); /// - public bool Equals(TaskClientType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AutopilotObjectiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34231,62 +36612,65 @@ public TaskClientType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AutopilotObjectiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AutopilotObjectiveStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); } } } -/// Active status a client owner may publish with a progress update. +/// Result of reloading a customization component. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientActiveStatus : IEquatable +public readonly struct CustomizationReloadStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public TaskClientActiveStatus(string value) + public CustomizationReloadStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The external owner is actively working. - public static TaskClientActiveStatus Running { get; } = new("running"); + /// The component was refreshed successfully. + public static CustomizationReloadStatus Reloaded { get; } = new("reloaded"); - /// The external owner is connected but waiting. - public static TaskClientActiveStatus Idle { get; } = new("idle"); + /// The component was not configured, loaded, or eligible for refresh. + public static CustomizationReloadStatus Skipped { get; } = new("skipped"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientActiveStatus left, TaskClientActiveStatus right) => left.Equals(right); + /// The component could not be refreshed; other components may still reload. + public static CustomizationReloadStatus Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskClientActiveStatus left, TaskClientActiveStatus right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CustomizationReloadStatus left, CustomizationReloadStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CustomizationReloadStatus left, CustomizationReloadStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); + public override bool Equals(object? obj) => obj is CustomizationReloadStatus other && Equals(other); /// - public bool Equals(TaskClientActiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CustomizationReloadStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34294,62 +36678,80 @@ public TaskClientActiveStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override TaskClientActiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CustomizationReloadStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, TaskClientActiveStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CustomizationReloadStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CustomizationReloadStatus)); } } } -/// Consumer allowed to call an MCP tool. +/// Component of session customization discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpToolUiVisibility : IEquatable +public readonly struct CustomizationReloadSubsystem : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpToolUiVisibility(string value) + public CustomizationReloadSubsystem(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The model may call the tool. - public static McpToolUiVisibility Model { get; } = new("model"); + /// Repository metadata and working-directory context. + public static CustomizationReloadSubsystem RepositoryContext { get; } = new("repositoryContext"); - /// An MCP App view may call the tool. - public static McpToolUiVisibility App { get; } = new("app"); + /// Session instructions and their cached dynamic context. + public static CustomizationReloadSubsystem Instructions { get; } = new("instructions"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpToolUiVisibility left, McpToolUiVisibility right) => left.Equals(right); + /// Discovered plugin configuration. + public static CustomizationReloadSubsystem Plugins { get; } = new("plugins"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpToolUiVisibility left, McpToolUiVisibility right) => !(left == right); + /// Configured session and plugin hooks. + public static CustomizationReloadSubsystem Hooks { get; } = new("hooks"); + + /// Discovered skills. + public static CustomizationReloadSubsystem Skills { get; } = new("skills"); + + /// Discovered custom agents. + public static CustomizationReloadSubsystem Agents { get; } = new("agents"); + + /// Loaded MCP server configuration. + public static CustomizationReloadSubsystem Mcp { get; } = new("mcp"); + + /// Configured session extensions. + public static CustomizationReloadSubsystem Extensions { get; } = new("extensions"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CustomizationReloadSubsystem left, CustomizationReloadSubsystem right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CustomizationReloadSubsystem left, CustomizationReloadSubsystem right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpToolUiVisibility other && Equals(other); + public override bool Equals(object? obj) => obj is CustomizationReloadSubsystem other && Equals(other); /// - public bool Equals(McpToolUiVisibility other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CustomizationReloadSubsystem other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34357,65 +36759,62 @@ public McpToolUiVisibility(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpToolUiVisibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CustomizationReloadSubsystem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpToolUiVisibility value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CustomizationReloadSubsystem value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CustomizationReloadSubsystem)); } } } -/// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. +/// Whether task execution is synchronously awaited or managed in the background. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpSamplingExecutionAction : IEquatable +public readonly struct TaskExecutionMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpSamplingExecutionAction(string value) + public TaskExecutionMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The sampling inference completed and produced a result. - public static McpSamplingExecutionAction Success { get; } = new("success"); - - /// The sampling inference failed or was rejected. - public static McpSamplingExecutionAction Failure { get; } = new("failure"); + /// The task was started with synchronous waiting. + public static TaskExecutionMode Sync { get; } = new("sync"); - /// The sampling inference was cancelled before completion. - public static McpSamplingExecutionAction Cancelled { get; } = new("cancelled"); + /// The task is managed in the background. + public static TaskExecutionMode Background { get; } = new("background"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskExecutionMode left, TaskExecutionMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskExecutionMode left, TaskExecutionMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpSamplingExecutionAction other && Equals(other); + public override bool Equals(object? obj) => obj is TaskExecutionMode other && Equals(other); /// - public bool Equals(McpSamplingExecutionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34423,62 +36822,71 @@ public McpSamplingExecutionAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpSamplingExecutionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpSamplingExecutionAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskExecutionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSamplingExecutionAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskExecutionMode)); } } } -/// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". +/// Current lifecycle status of the task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpSetEnvValueModeDetails : IEquatable +public readonly struct TaskStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpSetEnvValueModeDetails(string value) + public TaskStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Treat MCP server environment values as literal strings. - public static McpSetEnvValueModeDetails Direct { get; } = new("direct"); + /// The task is actively executing. + public static TaskStatus Running { get; } = new("running"); - /// Treat MCP server environment values as host-side references to resolve before launch. - public static McpSetEnvValueModeDetails Indirect { get; } = new("indirect"); + /// The task is waiting for additional input. + public static TaskStatus Idle { get; } = new("idle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => left.Equals(right); + /// The task finished successfully. + public static TaskStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => !(left == right); + /// The task finished with an error. + public static TaskStatus Failed { get; } = new("failed"); + + /// The task was cancelled before completion. + public static TaskStatus Cancelled { get; } = new("cancelled"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskStatus left, TaskStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskStatus left, TaskStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpSetEnvValueModeDetails other && Equals(other); + public override bool Equals(object? obj) => obj is TaskStatus other && Equals(other); /// - public bool Equals(McpSetEnvValueModeDetails other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34486,62 +36894,59 @@ public McpSetEnvValueModeDetails(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpSetEnvValueModeDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpSetEnvValueModeDetails value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSetEnvValueModeDetails)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskStatus)); } } } -/// Outcome of starting the original prepared owned login. +/// Client-owned tasks always execute outside the runtime in background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOwnedOauthLoginStatus : IEquatable +public readonly struct TaskClientExecutionMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOwnedOauthLoginStatus(string value) + public TaskClientExecutionMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The original requester may open the returned authorisation URL. - public static McpOwnedOauthLoginStatus AwaitingBrowser { get; } = new("awaiting-browser"); - - /// Cached credentials were accepted and the original server finished reconnecting. - public static McpOwnedOauthLoginStatus Connected { get; } = new("connected"); + /// Gets the background value. + public static TaskClientExecutionMode Background { get; } = new("background"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientExecutionMode left, TaskClientExecutionMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientExecutionMode left, TaskClientExecutionMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOwnedOauthLoginStatus other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientExecutionMode other && Equals(other); /// - public bool Equals(McpOwnedOauthLoginStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34549,62 +36954,62 @@ public McpOwnedOauthLoginStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOwnedOauthLoginStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOwnedOauthLoginStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientExecutionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOwnedOauthLoginStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); } } } -/// OAuth grant type override for this login. +/// Connection class owning a client task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthLoginGrantType : IEquatable +public readonly struct TaskClientOwnerKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOauthLoginGrantType(string value) + public TaskClientOwnerKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. - public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); + /// A discovered extension connection owns the task. + public static TaskClientOwnerKind Extension { get; } = new("extension"); - /// Headless OAuth flow where a confidential client authenticates directly with a client secret. - public static McpOauthLoginGrantType ClientCredentials { get; } = new("client_credentials"); + /// A generic SDK connection owns the task. + public static TaskClientOwnerKind Sdk { get; } = new("sdk"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerKind left, TaskClientOwnerKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOauthLoginGrantType other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); /// - public bool Equals(McpOauthLoginGrantType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientOwnerKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34612,65 +37017,62 @@ public McpOauthLoginGrantType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOauthLoginGrantType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientOwnerKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOauthLoginGrantType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientOwnerKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); } } } -/// Why a passive MCP OAuth probe determined authentication is needed. +/// Presence of the task's bound join. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthProbeNeedsAuthReason : IEquatable +public readonly struct TaskClientOwnerPresence : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpOauthProbeNeedsAuthReason(string value) + public TaskClientOwnerPresence(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// No token was sent and the server requires authentication. - public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); - - /// A cached token was sent and rejected. - public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); + /// The bound session join is connected. + public static TaskClientOwnerPresence Connected { get; } = new("connected"); - /// The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. - public static McpOauthProbeNeedsAuthReason Upscope { get; } = new("upscope"); + /// The bound session join is disconnected. + public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpOauthProbeNeedsAuthReason other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); /// - public bool Equals(McpOauthProbeNeedsAuthReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientOwnerPresence other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34678,65 +37080,74 @@ public McpOauthProbeNeedsAuthReason(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpOauthProbeNeedsAuthReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientOwnerPresence Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpOauthProbeNeedsAuthReason value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientOwnerPresence value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); } } } -/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. +/// Lifecycle status of a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsAvailableDisplayMode : IEquatable +public readonly struct TaskClientStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) + public TaskClientStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); + /// The external owner is actively working. + public static TaskClientStatus Running { get; } = new("running"); - /// Rendered as a fullscreen overlay. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The external owner is connected but waiting. + public static TaskClientStatus Idle { get; } = new("idle"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsSetHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); + /// The owner reported successful completion. + public static TaskClientStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + /// The owner reported failure. + public static TaskClientStatus Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => !(left == right); + /// The owner reported or confirmed cancellation. + public static TaskClientStatus Cancelled { get; } = new("cancelled"); + + /// The bound owner join disappeared; external executor state is unknown. + public static TaskClientStatus Orphaned { get; } = new("orphaned"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientStatus left, TaskClientStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientStatus left, TaskClientStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34744,65 +37155,62 @@ public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); } } } -/// Current display mode (SEP-1865). +/// Whether the shell runs inside a managed PTY session or as an independent background process. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsDisplayMode : IEquatable +public readonly struct TaskShellInfoAttachmentMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsDisplayMode(string value) + public TaskShellInfoAttachmentMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsSetHostContextDetailsDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsSetHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The shell runs in a managed PTY session. + public static TaskShellInfoAttachmentMode Attached { get; } = new("attached"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// The shell runs as an independent background process. + public static TaskShellInfoAttachmentMode Detached { get; } = new("detached"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskShellInfoAttachmentMode other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskShellInfoAttachmentMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34810,65 +37218,59 @@ public McpAppsSetHostContextDetailsDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskShellInfoAttachmentMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskShellInfoAttachmentMode)); } } } -/// Platform type for responsive design. +/// Discriminator for a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsPlatform : IEquatable +public readonly struct TaskClientType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsPlatform(string value) + public TaskClientType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Host runs in a web browser. - public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); - - /// Host runs as a desktop application. - public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); - - /// Host runs on a mobile device. - public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// Gets the client value. + public static TaskClientType Client { get; } = new("client"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34876,62 +37278,62 @@ public McpAppsSetHostContextDetailsPlatform(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); } } } -/// UI theme preference per SEP-1865. +/// Active status a client owner may publish with a progress update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsTheme : IEquatable +public readonly struct TaskClientActiveStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsSetHostContextDetailsTheme(string value) + public TaskClientActiveStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Light UI theme. - public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); + /// The external owner is actively working. + public static TaskClientActiveStatus Running { get; } = new("running"); - /// Dark UI theme. - public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); + /// The external owner is connected but waiting. + public static TaskClientActiveStatus Idle { get; } = new("idle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientActiveStatus left, TaskClientActiveStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientActiveStatus left, TaskClientActiveStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(TaskClientActiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -34939,65 +37341,62 @@ public McpAppsSetHostContextDetailsTheme(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsSetHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TaskClientActiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, TaskClientActiveStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); } } } -/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. +/// Consumer allowed to call an MCP tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsAvailableDisplayMode : IEquatable +public readonly struct McpToolUiVisibility : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsAvailableDisplayMode(string value) + public McpToolUiVisibility(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The model may call the tool. + public static McpToolUiVisibility Model { get; } = new("model"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); + /// An MCP App view may call the tool. + public static McpToolUiVisibility App { get; } = new("app"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpToolUiVisibility left, McpToolUiVisibility right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpToolUiVisibility left, McpToolUiVisibility right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpToolUiVisibility other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpToolUiVisibility other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35005,65 +37404,65 @@ public McpAppsHostContextDetailsAvailableDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpToolUiVisibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpToolUiVisibility value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); } } } -/// Current display mode (SEP-1865). +/// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsDisplayMode : IEquatable +public readonly struct McpSamplingExecutionAction : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsDisplayMode(string value) + public McpSamplingExecutionAction(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Rendered inline within the host conversation surface. - public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); + /// The sampling inference completed and produced a result. + public static McpSamplingExecutionAction Success { get; } = new("success"); - /// Rendered as a fullscreen overlay. - public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + /// The sampling inference failed or was rejected. + public static McpSamplingExecutionAction Failure { get; } = new("failure"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// The sampling inference was cancelled before completion. + public static McpSamplingExecutionAction Cancelled { get; } = new("cancelled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpSamplingExecutionAction other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpSamplingExecutionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35071,65 +37470,62 @@ public McpAppsHostContextDetailsDisplayMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpSamplingExecutionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpSamplingExecutionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSamplingExecutionAction)); } } } -/// Platform type for responsive design. +/// How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsPlatform : IEquatable +public readonly struct McpSetEnvValueModeDetails : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsPlatform(string value) + public McpSetEnvValueModeDetails(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Host runs in a web browser. - public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); - - /// Host runs as a desktop application. - public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); + /// Treat MCP server environment values as literal strings. + public static McpSetEnvValueModeDetails Direct { get; } = new("direct"); - /// Host runs on a mobile device. - public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// Treat MCP server environment values as host-side references to resolve before launch. + public static McpSetEnvValueModeDetails Indirect { get; } = new("indirect"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is McpSetEnvValueModeDetails other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpSetEnvValueModeDetails other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35137,62 +37533,62 @@ public McpAppsHostContextDetailsPlatform(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpSetEnvValueModeDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpSetEnvValueModeDetails value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSetEnvValueModeDetails)); } } } -/// UI theme preference per SEP-1865. +/// Outcome of starting the original prepared owned login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsTheme : IEquatable +public readonly struct McpOwnedOauthLoginStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpAppsHostContextDetailsTheme(string value) + public McpOwnedOauthLoginStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Light UI theme. - public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); + /// The original requester may open the returned authorisation URL. + public static McpOwnedOauthLoginStatus AwaitingBrowser { get; } = new("awaiting-browser"); - /// Dark UI theme. - public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); + /// Cached credentials were accepted and the original server finished reconnecting. + public static McpOwnedOauthLoginStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is McpOwnedOauthLoginStatus other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOwnedOauthLoginStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35200,74 +37596,62 @@ public McpAppsHostContextDetailsTheme(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpAppsHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOwnedOauthLoginStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOwnedOauthLoginStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOwnedOauthLoginStatus)); } } } -/// Session-scoped diagnostic threshold. Capture is disabled by default and is never persisted with the session. +/// OAuth grant type override for this login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticLogLevel : IEquatable +public readonly struct McpOauthLoginGrantType : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticLogLevel(string value) + public McpOauthLoginGrantType(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Disable capture and clear retained diagnostics. - public static DiagnosticLogLevel Off { get; } = new("off"); - - /// Capture failures only. - public static DiagnosticLogLevel Error { get; } = new("error"); - - /// Capture failures, warnings, and stderr. - public static DiagnosticLogLevel Warning { get; } = new("warning"); - - /// Capture lifecycle diagnostics. - public static DiagnosticLogLevel Info { get; } = new("info"); - - /// Capture protocol frames and launch diagnostics. - public static DiagnosticLogLevel Debug { get; } = new("debug"); + /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. + public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); - /// Capture HTTP metadata in addition to debug diagnostics. - public static DiagnosticLogLevel Trace { get; } = new("trace"); + /// Headless OAuth flow where a confidential client authenticates directly with a client secret. + public static McpOauthLoginGrantType ClientCredentials { get; } = new("client_credentials"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticLogLevel left, DiagnosticLogLevel right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticLogLevel left, DiagnosticLogLevel right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOauthLoginGrantType left, McpOauthLoginGrantType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthLoginGrantType other && Equals(other); /// - public bool Equals(DiagnosticLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOauthLoginGrantType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35275,62 +37659,65 @@ public DiagnosticLogLevel(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOauthLoginGrantType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticLogLevel value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOauthLoginGrantType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); } } } -/// Whether the supplied diagnostic cursor remained within the retained buffer window. +/// Why a passive MCP OAuth probe determined authentication is needed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticCursorStatus : IEquatable +public readonly struct McpOauthProbeNeedsAuthReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticCursorStatus(string value) + public McpOauthProbeNeedsAuthReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The cursor is valid for the current retained window. - public static DiagnosticCursorStatus Ok { get; } = new("ok"); + /// No token was sent and the server requires authentication. + public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); + + /// A cached token was sent and rejected. + public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); - /// The cursor no longer addresses retained records; reading resumes at the oldest retained record. - public static DiagnosticCursorStatus Expired { get; } = new("expired"); + /// The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. + public static McpOauthProbeNeedsAuthReason Upscope { get; } = new("upscope"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticCursorStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthProbeNeedsAuthReason other && Equals(other); /// - public bool Equals(DiagnosticCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpOauthProbeNeedsAuthReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35338,62 +37725,65 @@ public DiagnosticCursorStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpOauthProbeNeedsAuthReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticCursorStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpOauthProbeNeedsAuthReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); } } } -/// Direction of an observed MCP protocol frame. +/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticDirection : IEquatable +public readonly struct McpAppsSetHostContextDetailsAvailableDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpDiagnosticDirection(string value) + public McpAppsSetHostContextDetailsAvailableDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Frame emitted by the Copilot MCP client. - public static McpDiagnosticDirection ClientToServer { get; } = new("client-to-server"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// Frame received from the MCP server. - public static McpDiagnosticDirection ServerToClient { get; } = new("server-to-client"); + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpDiagnosticDirection left, McpDiagnosticDirection right) => left.Equals(right); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpDiagnosticDirection left, McpDiagnosticDirection right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpDiagnosticDirection other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(McpDiagnosticDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35401,68 +37791,65 @@ public McpDiagnosticDirection(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpDiagnosticDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpDiagnosticDirection value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticDirection)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); } } } -/// Category for an MCP diagnostic record. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticKind : IEquatable +public readonly struct McpAppsSetHostContextDetailsDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public McpDiagnosticKind(string value) + public McpAppsSetHostContextDetailsDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Connection lifecycle transition or failure. - public static McpDiagnosticKind Lifecycle { get; } = new("lifecycle"); - - /// JSON-RPC protocol frame. - public static McpDiagnosticKind Protocol { get; } = new("protocol"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsDisplayMode Inline { get; } = new("inline"); - /// HTTP request or response metadata. - public static McpDiagnosticKind Http { get; } = new("http"); + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Local MCP server standard-error output. - public static McpDiagnosticKind Stderr { get; } = new("stderr"); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpDiagnosticKind left, McpDiagnosticKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpDiagnosticKind left, McpDiagnosticKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpDiagnosticKind other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(McpDiagnosticKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35470,71 +37857,65 @@ public McpDiagnosticKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override McpDiagnosticKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, McpDiagnosticKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); } } } -/// Severity of an emitted diagnostic record. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSeverity : IEquatable +public readonly struct McpAppsSetHostContextDetailsPlatform : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticSeverity(string value) + public McpAppsSetHostContextDetailsPlatform(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Failure that prevented or interrupted communication. - public static DiagnosticSeverity Error { get; } = new("error"); - - /// A recoverable warning or server standard-error output. - public static DiagnosticSeverity Warning { get; } = new("warning"); - - /// Lifecycle transition. - public static DiagnosticSeverity Info { get; } = new("info"); + /// Host runs in a web browser. + public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); - /// Protocol-frame or launch diagnostic. - public static DiagnosticSeverity Debug { get; } = new("debug"); + /// Host runs as a desktop application. + public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// HTTP metadata diagnostic. - public static DiagnosticSeverity Trace { get; } = new("trace"); + /// Host runs on a mobile device. + public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticSeverity left, DiagnosticSeverity right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticSeverity left, DiagnosticSeverity right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(DiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35542,59 +37923,62 @@ public DiagnosticSeverity(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticSeverity value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSeverity)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); } } } -/// A supported diagnostic source. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSource : IEquatable +public readonly struct McpAppsSetHostContextDetailsTheme : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public DiagnosticSource(string value) + public McpAppsSetHostContextDetailsTheme(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the mcp value. - public static DiagnosticSource Mcp { get; } = new("mcp"); + /// Light UI theme. + public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticSource left, DiagnosticSource right) => left.Equals(right); + /// Dark UI theme. + public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticSource left, DiagnosticSource right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticSource other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); /// - public bool Equals(DiagnosticSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35602,65 +37986,65 @@ public DiagnosticSource(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override DiagnosticSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, DiagnosticSource value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); } } } -/// Availability of the EXPERIMENTAL session connector API. +/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAvailability : IEquatable +public readonly struct McpAppsHostContextDetailsAvailableDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorAvailability(string value) + public McpAppsHostContextDetailsAvailableDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The resolved Connector feature is enabled and Connector requests are permitted. - public static ConnectorAvailability Enabled { get; } = new("enabled"); + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// The resolved Connector feature is off. No Connector service request is made while disabled. - public static ConnectorAvailability Disabled { get; } = new("disabled"); + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// The session has no eligible host-owned GitHub account or does not support local Connector projection. - public static ConnectorAvailability Unavailable { get; } = new("unavailable"); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorAvailability left, ConnectorAvailability right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(ConnectorAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35668,59 +38052,65 @@ public ConnectorAvailability(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorAvailability value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); } } } -/// Stable OAuth scope whose absence prevents Connector management. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAuthorizationScope : IEquatable +public readonly struct McpAppsHostContextDetailsDisplayMode : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorAuthorizationScope(string value) + public McpAppsHostContextDetailsDisplayMode(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Allows Copilot to manage Connector connections and use their tools. - public static ConnectorAuthorizationScope WritePluginGatewayConnections { get; } = new("write_plugin_gateway_connections"); + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => left.Equals(right); + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => !(left == right); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorAuthorizationScope other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(ConnectorAuthorizationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35728,71 +38118,65 @@ public ConnectorAuthorizationScope(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorAuthorizationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); } } } -/// Authoritative service connection state for one Connector. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorCatalogStatus : IEquatable +public readonly struct McpAppsHostContextDetailsPlatform : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorCatalogStatus(string value) + public McpAppsHostContextDetailsPlatform(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The Connector is available but not connected. - public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - - /// The Connector service is still completing connection or consent. - public static ConnectorCatalogStatus Pending { get; } = new("pending"); - - /// The Connector is connected and may contribute MCP servers. - public static ConnectorCatalogStatus Connected { get; } = new("connected"); + /// Host runs in a web browser. + public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); - /// The Connector service reports an unusable connection. - public static ConnectorCatalogStatus Error { get; } = new("error"); + /// Host runs as a desktop application. + public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// The service returned a future or unrecognized state. - public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + /// Host runs on a mobile device. + public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsPlatform other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35800,77 +38184,62 @@ public ConnectorCatalogStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); } } } -/// Live MCP status of one Connector-owned runtime server. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorMcpStatus : IEquatable +public readonly struct McpAppsHostContextDetailsTheme : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ConnectorMcpStatus(string value) + public McpAppsHostContextDetailsTheme(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The server is connected and its tools are available. - public static ConnectorMcpStatus Connected { get; } = new("connected"); - - /// The server connection is still being established. - public static ConnectorMcpStatus Pending { get; } = new("pending"); - - /// The server requires refreshed GitHub authorization. - public static ConnectorMcpStatus NeedsAuth { get; } = new("needs_auth"); - - /// The server failed to connect or initialize. - public static ConnectorMcpStatus Failed { get; } = new("failed"); - - /// The server is intentionally stopped, including when managed policy blocks it. - public static ConnectorMcpStatus Stopped { get; } = new("stopped"); - - /// The server is configured but explicitly disabled. - public static ConnectorMcpStatus Disabled { get; } = new("disabled"); + /// Light UI theme. + public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); - /// The Connector currently has no live server configuration. - public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + /// Dark UI theme. + public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorMcpStatus left, ConnectorMcpStatus right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorMcpStatus left, ConnectorMcpStatus right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorMcpStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); /// - public bool Equals(ConnectorMcpStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsHostContextDetailsTheme other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35878,62 +38247,62 @@ public ConnectorMcpStatus(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ConnectorMcpStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ConnectorMcpStatus value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); } } } -/// Transport to be used for provider requests. +/// The sender role of an MCP prompt message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointTransport : IEquatable +public readonly struct McpPromptRole : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointTransport(string value) + public McpPromptRole(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// HTTP request/streaming transport. - public static ProviderEndpointTransport Http { get; } = new("http"); + /// A message from the user. + public static McpPromptRole User { get; } = new("user"); - /// WebSocket transport. - public static ProviderEndpointTransport Websockets { get; } = new("websockets"); + /// A message from the assistant. + public static McpPromptRole Assistant { get; } = new("assistant"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointTransport left, ProviderEndpointTransport right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPromptRole left, McpPromptRole right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointTransport left, ProviderEndpointTransport right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPromptRole left, McpPromptRole right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointTransport other && Equals(other); + public override bool Equals(object? obj) => obj is McpPromptRole other && Equals(other); /// - public bool Equals(ProviderEndpointTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpPromptRole other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -35941,65 +38310,74 @@ public ProviderEndpointTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpPromptRole Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpPromptRole value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPromptRole)); } } } -/// Provider family. Matches the `type` field of a BYOK provider config. +/// Session-scoped diagnostic threshold. Capture is disabled by default and is never persisted with the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointType : IEquatable +public readonly struct DiagnosticLogLevel : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointType(string value) + public DiagnosticLogLevel(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OpenAI-compatible endpoint (use the OpenAI client library). - public static ProviderEndpointType Openai { get; } = new("openai"); + /// Disable capture and clear retained diagnostics. + public static DiagnosticLogLevel Off { get; } = new("off"); - /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). - public static ProviderEndpointType Azure { get; } = new("azure"); + /// Capture failures only. + public static DiagnosticLogLevel Error { get; } = new("error"); - /// Anthropic endpoint (use the Anthropic client library). - public static ProviderEndpointType Anthropic { get; } = new("anthropic"); + /// Capture failures, warnings, and stderr. + public static DiagnosticLogLevel Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointType left, ProviderEndpointType right) => left.Equals(right); + /// Capture lifecycle diagnostics. + public static DiagnosticLogLevel Info { get; } = new("info"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointType left, ProviderEndpointType right) => !(left == right); + /// Capture protocol frames and launch diagnostics. + public static DiagnosticLogLevel Debug { get; } = new("debug"); + + /// Capture HTTP metadata in addition to debug diagnostics. + public static DiagnosticLogLevel Trace { get; } = new("trace"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticLogLevel left, DiagnosticLogLevel right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticLogLevel left, DiagnosticLogLevel right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointType other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticLogLevel other && Equals(other); /// - public bool Equals(ProviderEndpointType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticLogLevel other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36007,62 +38385,62 @@ public ProviderEndpointType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticLogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticLogLevel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); } } } -/// Wire API to be used, when required for the provider type. +/// Whether the supplied diagnostic cursor remained within the retained buffer window. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointWireApi : IEquatable +public readonly struct DiagnosticCursorStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderEndpointWireApi(string value) + public DiagnosticCursorStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Classic chat-completions request shape. - public static ProviderEndpointWireApi Completions { get; } = new("completions"); + /// The cursor is valid for the current retained window. + public static DiagnosticCursorStatus Ok { get; } = new("ok"); - /// Newer responses request shape. - public static ProviderEndpointWireApi Responses { get; } = new("responses"); + /// The cursor no longer addresses retained records; reading resumes at the oldest retained record. + public static DiagnosticCursorStatus Expired { get; } = new("expired"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointWireApi left, ProviderEndpointWireApi right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticCursorStatus left, DiagnosticCursorStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticCursorStatus other && Equals(other); /// - public bool Equals(ProviderEndpointWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36070,59 +38448,62 @@ public ProviderEndpointWireApi(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderEndpointWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderEndpointWireApi value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticCursorStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); } } } -/// Defines the allowed values. +/// Direction of an observed MCP protocol frame. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolAppendMode : IEquatable +public readonly struct McpDiagnosticDirection : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolAppendMode(string value) + public McpDiagnosticDirection(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the append value. - public static ProtocolAppendMode Append { get; } = new("append"); + /// Frame emitted by the Copilot MCP client. + public static McpDiagnosticDirection ClientToServer { get; } = new("client-to-server"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolAppendMode left, ProtocolAppendMode right) => left.Equals(right); + /// Frame received from the MCP server. + public static McpDiagnosticDirection ServerToClient { get; } = new("server-to-client"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolAppendMode left, ProtocolAppendMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpDiagnosticDirection left, McpDiagnosticDirection right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpDiagnosticDirection left, McpDiagnosticDirection right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolAppendMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticDirection other && Equals(other); /// - public bool Equals(ProtocolAppendMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpDiagnosticDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36130,59 +38511,68 @@ public ProtocolAppendMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolAppendMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpDiagnosticDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolAppendMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpDiagnosticDirection value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticDirection)); } } } -/// Defines the allowed values. +/// Category for an MCP diagnostic record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolReplaceMode : IEquatable +public readonly struct McpDiagnosticKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolReplaceMode(string value) + public McpDiagnosticKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the replace value. - public static ProtocolReplaceMode Replace { get; } = new("replace"); + /// Connection lifecycle transition or failure. + public static McpDiagnosticKind Lifecycle { get; } = new("lifecycle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolReplaceMode left, ProtocolReplaceMode right) => left.Equals(right); + /// JSON-RPC protocol frame. + public static McpDiagnosticKind Protocol { get; } = new("protocol"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolReplaceMode left, ProtocolReplaceMode right) => !(left == right); + /// HTTP request or response metadata. + public static McpDiagnosticKind Http { get; } = new("http"); + + /// Local MCP server standard-error output. + public static McpDiagnosticKind Stderr { get; } = new("stderr"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpDiagnosticKind left, McpDiagnosticKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpDiagnosticKind left, McpDiagnosticKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolReplaceMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticKind other && Equals(other); /// - public bool Equals(ProtocolReplaceMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpDiagnosticKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36190,59 +38580,71 @@ public ProtocolReplaceMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolReplaceMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpDiagnosticKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolReplaceMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpDiagnosticKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticKind)); } } } -/// Defines the allowed values. +/// Severity of an emitted diagnostic record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolCustomizeMode : IEquatable +public readonly struct DiagnosticSeverity : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolCustomizeMode(string value) + public DiagnosticSeverity(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Gets the customize value. - public static ProtocolCustomizeMode Customize { get; } = new("customize"); + /// Failure that prevented or interrupted communication. + public static DiagnosticSeverity Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => left.Equals(right); + /// A recoverable warning or server standard-error output. + public static DiagnosticSeverity Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => !(left == right); + /// Lifecycle transition. + public static DiagnosticSeverity Info { get; } = new("info"); + + /// Protocol-frame or launch diagnostic. + public static DiagnosticSeverity Debug { get; } = new("debug"); + + /// HTTP metadata diagnostic. + public static DiagnosticSeverity Trace { get; } = new("trace"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticSeverity left, DiagnosticSeverity right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticSeverity left, DiagnosticSeverity right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolCustomizeMode other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); /// - public bool Equals(ProtocolCustomizeMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticSeverity other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36250,68 +38652,59 @@ public ProtocolCustomizeMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolCustomizeMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolCustomizeMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticSeverity value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSeverity)); } } } -/// Defines the allowed values. +/// A supported diagnostic source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolStaticSectionAction : IEquatable +public readonly struct DiagnosticSource : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProtocolStaticSectionAction(string value) + public DiagnosticSource(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Replace the section content. - public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - - /// Remove the section content. - public static ProtocolStaticSectionAction Remove { get; } = new("remove"); - - /// Append content to the section. - public static ProtocolStaticSectionAction Append { get; } = new("append"); - - /// Prepend content to the section. - public static ProtocolStaticSectionAction Prepend { get; } = new("prepend"); + /// Gets the mcp value. + public static DiagnosticSource Mcp { get; } = new("mcp"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiagnosticSource left, DiagnosticSource right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProtocolStaticSectionAction left, ProtocolStaticSectionAction right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiagnosticSource left, DiagnosticSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSource other && Equals(other); /// - public bool Equals(ProtocolStaticSectionAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(DiagnosticSource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36319,62 +38712,65 @@ public ProtocolStaticSectionAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProtocolStaticSectionAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DiagnosticSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProtocolStaticSectionAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DiagnosticSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); } } } -/// Provider transport. Defaults to "http". +/// Availability of the EXPERIMENTAL session connector API. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigTransport : IEquatable +public readonly struct ConnectorAvailability : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigTransport(string value) + public ConnectorAvailability(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// HTTP request/streaming transport. - public static ProviderConfigTransport Http { get; } = new("http"); + /// The resolved Connector feature is enabled and Connector requests are permitted. + public static ConnectorAvailability Enabled { get; } = new("enabled"); - /// WebSocket transport. - public static ProviderConfigTransport Websockets { get; } = new("websockets"); + /// The resolved Connector feature is off. No Connector service request is made while disabled. + public static ConnectorAvailability Disabled { get; } = new("disabled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigTransport left, ProviderConfigTransport right) => left.Equals(right); + /// The session has no eligible host-owned GitHub account or does not support local Connector projection. + public static ConnectorAvailability Unavailable { get; } = new("unavailable"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigTransport left, ProviderConfigTransport right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorAvailability left, ConnectorAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigTransport other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); /// - public bool Equals(ProviderConfigTransport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36382,65 +38778,59 @@ public ProviderConfigTransport(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigTransport value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorAvailability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); } } } -/// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. +/// Stable OAuth scope whose absence prevents Connector management. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigType : IEquatable +public readonly struct ConnectorAuthorizationScope : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigType(string value) + public ConnectorAuthorizationScope(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Generic OpenAI-compatible API. - public static ProviderConfigType Openai { get; } = new("openai"); - - /// Azure OpenAI Service endpoint. - public static ProviderConfigType Azure { get; } = new("azure"); - - /// Anthropic API endpoint. - public static ProviderConfigType Anthropic { get; } = new("anthropic"); + /// Allows Copilot to manage Connector connections and use their tools. + public static ConnectorAuthorizationScope WritePluginGatewayConnections { get; } = new("write_plugin_gateway_connections"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigType left, ProviderConfigType right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigType other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAuthorizationScope other && Equals(other); /// - public bool Equals(ProviderConfigType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorAuthorizationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36448,62 +38838,77 @@ public ProviderConfigType(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorAuthorizationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigType value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorAuthorizationScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); } } } -/// Wire API format (openai/azure only). Defaults to "completions". +/// Live MCP status of one Connector-owned runtime server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigWireApi : IEquatable +public readonly struct ConnectorMcpStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public ProviderConfigWireApi(string value) + public ConnectorMcpStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// OpenAI Chat Completions wire format. - public static ProviderConfigWireApi Completions { get; } = new("completions"); + /// The server is connected and its tools are available. + public static ConnectorMcpStatus Connected { get; } = new("connected"); - /// OpenAI Responses API wire format. - public static ProviderConfigWireApi Responses { get; } = new("responses"); + /// The server connection is still being established. + public static ConnectorMcpStatus Pending { get; } = new("pending"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigWireApi left, ProviderConfigWireApi right) => left.Equals(right); + /// The server requires refreshed GitHub authorization. + public static ConnectorMcpStatus NeedsAuth { get; } = new("needs_auth"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigWireApi left, ProviderConfigWireApi right) => !(left == right); + /// The server failed to connect or initialize. + public static ConnectorMcpStatus Failed { get; } = new("failed"); + + /// The server is intentionally stopped, including when managed policy blocks it. + public static ConnectorMcpStatus Stopped { get; } = new("stopped"); + + /// The server is configured but explicitly disabled. + public static ConnectorMcpStatus Disabled { get; } = new("disabled"); + + /// The Connector currently has no live server configuration. + public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorMcpStatus left, ConnectorMcpStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorMcpStatus left, ConnectorMcpStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderConfigWireApi other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorMcpStatus other && Equals(other); /// - public bool Equals(ProviderConfigWireApi other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(ConnectorMcpStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -36511,20 +38916,20 @@ public ProviderConfigWireApi(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProviderConfigWireApi Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ConnectorMcpStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, ProviderConfigWireApi value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConnectorMcpStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigWireApi)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); } } } @@ -38837,69 +41242,6 @@ public override void Write(Utf8JsonWriter writer, WorkspaceSummaryHostType value } -/// Hosting platform type of the repository. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct SessionWorkingDirectoryContextHostType : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public SessionWorkingDirectoryContextHostType(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The working directory repository is hosted on GitHub. - public static SessionWorkingDirectoryContextHostType GitHub { get; } = new("github"); - - /// The working directory repository is hosted on Azure DevOps. - public static SessionWorkingDirectoryContextHostType Ado { get; } = new("ado"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionWorkingDirectoryContextHostType left, SessionWorkingDirectoryContextHostType right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is SessionWorkingDirectoryContextHostType other && Equals(other); - - /// - public bool Equals(SessionWorkingDirectoryContextHostType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override SessionWorkingDirectoryContextHostType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, SessionWorkingDirectoryContextHostType value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionWorkingDirectoryContextHostType)); - } - } -} - - /// Rust-owned settings predicates exposed across the SDK boundary. Raw feature-flag names are intentionally not part of the contract. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -40649,6 +42991,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + /// GlobalState APIs. + public ServerGlobalStateApi GlobalState => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Commands APIs. public ServerCommandsApi Commands => field ?? @@ -40661,6 +43009,24 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; + /// GitHubRepository APIs. + public ServerGitHubRepositoryApi GitHubRepository => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + + /// GitHubOwners APIs. + public ServerGitHubOwnersApi GitHubOwners => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + + /// Git APIs. + public ServerGitApi Git => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// ManagedSettings APIs. public ServerManagedSettingsApi ManagedSettings => field ?? @@ -40697,8 +43063,8 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; - /// Accounts APIs. - public ServerAccountsApi Accounts => + /// Connectors APIs. + public ServerConnectorsApi Connectors => field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; @@ -40932,6 +43298,72 @@ public async Task GetHostSupportAsync(CancellationToken canc { return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.getHostSupport", [], cancellationToken); } + + /// ProxyCa APIs. + public ServerSandboxProxyCaApi ProxyCa => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; +} + +/// Provides server-scoped SandboxProxyCa APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerSandboxProxyCaApi +{ + private readonly JsonRpc _rpc; + + internal ServerSandboxProxyCaApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task GetStatusAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.getStatus", [request], cancellationToken); + } + + /// Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Result of creating the persistent certificate authority of the sandbox credential proxy. + public async Task CreateAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.create", [request], cancellationToken); + } + + /// Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task RotateAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.rotate", [request], cancellationToken); + } + + /// Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task TrustAsync(SandboxConfig? sandboxConfig = null, CancellationToken cancellationToken = default) + { + var request = new SandboxProxyCaRequest { SandboxConfig = sandboxConfig }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.trust", [request], cancellationToken); + } + + /// Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. + /// The to monitor for cancellation requests. The default is . + /// Status of the persistent certificate authority of the sandbox credential proxy. + public async Task RemoveAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "sandbox.proxyCa.remove", [], cancellationToken); + } } /// Provides server-scoped Tools APIs. @@ -41898,6 +44330,64 @@ public async Task GetDiscoveryPathsAsync(IList? var request = new AgentsGetDiscoveryPathsRequest { ProjectPaths = projectPaths, ExcludeHostAgents = excludeHostAgents }; return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getDiscoveryPaths", [request], cancellationToken); } + + /// Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + /// The to monitor for cancellation requests. The default is . + /// The agents this runtime ships, named so a consumer can tell them apart from authored ones. + internal async Task GetBuiltinsAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltins", [], cancellationToken); + } + + /// Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + /// The to monitor for cancellation requests. The default is . + /// The shipped agents available under the requested flags. + internal async Task GetAvailableBuiltinsAsync(IDictionary? featureFlags = null, IDictionary? overrides = null, string? context = null, CancellationToken cancellationToken = default) + { + var request = new AgentsGetAvailableBuiltinsRequest { FeatureFlags = featureFlags, Overrides = overrides, Context = context }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getAvailableBuiltins", [request], cancellationToken); + } + + /// Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + /// The to monitor for cancellation requests. The default is . + /// One shipped agent's definition. + internal async Task GetBuiltinDefinitionAsync(string name, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(name); + + var request = new AgentsGetBuiltinDefinitionRequest { Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinDefinition", [request], cancellationToken); + } + + /// Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. + /// The to monitor for cancellation requests. The default is . + /// One shipped agent, projected for a listing. + internal async Task GetBuiltinListingDefinitionAsync(string name, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(name); + + var request = new AgentsGetBuiltinListingDefinitionRequest { Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.getBuiltinListingDefinition", [request], cancellationToken); + } + + /// Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + /// The models available to this session, serialized in the shape the model list carries. + /// The to monitor for cancellation requests. The default is . + /// The model to switch to, and the warning to show when the agent's preference could not be met. + internal async Task CustomAgentInitialModelDecisionAsync(string agentModelsJson, string availableModelsJson, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agentModelsJson); + ArgumentNullException.ThrowIfNull(availableModelsJson); + + var request = new AgentsCustomAgentInitialModelDecisionParams { AgentModelsJson = agentModelsJson, AvailableModelsJson = availableModelsJson }; + return await CopilotClient.InvokeRpcAsync(_rpc, "agents.customAgentInitialModelDecision", [request], cancellationToken); + } } /// Provides server-scoped Instructions APIs. @@ -41934,6 +44424,49 @@ public async Task GetDiscoveryPathsAsync(IListProvides server-scoped GlobalState APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGlobalStateApi +{ + private readonly JsonRpc _rpc; + + internal ServerGlobalStateApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. + /// The to monitor for cancellation requests. The default is . + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + internal async Task LoadAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.load", [], cancellationToken); + } + + /// Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + /// The to monitor for cancellation requests. The default is . + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. + internal async Task LoadForConfigDirAsync(string? configDir = null, CancellationToken cancellationToken = default) + { + var request = new GlobalStateLoadForConfigDirRequest { ConfigDir = configDir }; + return await CopilotClient.InvokeRpcAsync(_rpc, "globalState.loadForConfigDir", [request], cancellationToken); + } + + /// Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + /// The to monitor for cancellation requests. The default is . + internal async Task WriteKeyAsync(string key, string? configDir = null, object? value = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(key); + + var request = new GlobalStateWriteKeyRequest { Key = key, ConfigDir = configDir, Value = CopilotClient.ToJsonElementForWire(value) }; + await CopilotClient.InvokeRpcAsync(_rpc, "globalState.writeKey", [request], cancellationToken); + } +} + /// Provides server-scoped Commands APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class ServerCommandsApi @@ -41983,31 +44516,138 @@ internal ServerUserSettingsApi(JsonRpc rpc) _rpc = rpc; } - /// Drops this runtime process's in-memory user settings cache so the next settings read observes disk. + /// Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. /// The to monitor for cancellation requests. The default is . - public async Task ReloadAsync(CancellationToken cancellationToken = default) - { - await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.reload", [], cancellationToken); - } - - /// Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. - /// The to monitor for cancellation requests. The default is . - /// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + /// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. public async Task GetAsync(CancellationToken cancellationToken = default) { return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.get", [], cancellationToken); } - /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. + /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. /// Partial user settings to write, as a free-form object keyed by setting name. /// The to monitor for cancellation requests. The default is . - /// Outcome of writing user settings. - public async Task SetAsync(object settings, CancellationToken cancellationToken = default) + public async Task SetAsync(object settings, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(settings); var request = new UserSettingsSetRequest { Settings = CopilotClient.ToJsonElementForWire(settings)!.Value }; - return await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken); + await CopilotClient.InvokeRpcAsync(_rpc, "user.settings.set", [request], cancellationToken); + } +} + +/// Provides server-scoped GitHubRepository APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitHubRepositoryApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitHubRepositoryApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. + /// Absolute path to a directory inside the git working tree to resolve. + /// The to monitor for cancellation requests. The default is . + /// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. + internal async Task AtPathAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(path); + + var request = new GitHubRepositoryAtPathRequest { Path = path }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubRepository.atPath", [request], cancellationToken); + } +} + +/// Provides server-scoped GitHubOwners APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitHubOwnersApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitHubOwnersApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + /// The to monitor for cancellation requests. The default is . + /// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + internal async Task NextRequestIdAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.nextRequestId", [], cancellationToken); + } + + /// Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + /// The to monitor for cancellation requests. The default is . + /// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. + internal async Task ListAsync(long requestId, object authInfo, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(authInfo); + + var request = new GitHubOwnersListRequest { RequestId = requestId, AuthInfo = CopilotClient.ToJsonElementForWire(authInfo)!.Value }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.list", [request], cancellationToken); + } + + /// Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. + /// Request id the listing was started with. + /// The to monitor for cancellation requests. The default is . + /// Whether the id named a running owner listing. + internal async Task CancelAsync(long requestId, CancellationToken cancellationToken = default) + { + var request = new GitHubOwnersCancelRequest { RequestId = requestId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "gitHubOwners.cancel", [request], cancellationToken); + } +} + +/// Provides server-scoped Git APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerGitApi +{ + private readonly JsonRpc _rpc; + + internal ServerGitApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Reads the remote that the branch checked out in a working tree tracks, as `branch.<name>.remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// Absolute path to a directory inside the git working tree to query. + /// The to monitor for cancellation requests. The default is . + /// The remote the checked-out branch tracks. + internal async Task CurrentBranchRemoteAsync(string cwd, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(cwd); + + var request = new GitCwdRequest { Cwd = cwd }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.currentBranchRemote", [request], cancellationToken); + } + + /// Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. + /// Absolute path to a directory inside the git working tree to query. + /// The to monitor for cancellation requests. The default is . + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + internal async Task WorkingDirectoryContextAsync(string cwd, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(cwd); + + var request = new GitCwdRequest { Cwd = cwd }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.workingDirectoryContext", [request], cancellationToken); + } + + /// Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// Absolute path to the root of the git working tree. + /// The to monitor for cancellation requests. The default is . + /// The GitHub repositories a working tree's remotes point at. + internal async Task ReposFromRemotesAsync(string gitRoot, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(gitRoot); + + var request = new GitReposFromRemotesRequest { GitRoot = gitRoot }; + return await CopilotClient.InvokeRpcAsync(_rpc, "git.reposFromRemotes", [request], cancellationToken); } } @@ -42455,6 +45095,54 @@ public async Task EnrichMetadataAsync(IList(_rpc, "sessions.enrichMetadata", [request], cancellationToken); } + /// Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + /// Session ID the workspace record belongs to. + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + /// User-supplied display name for the workspace. + /// The to monitor for cancellation requests. The default is . + /// The workspace record that was written. + internal async Task CreateWorkspaceAsync(string sessionId, string sessionStatePath, string convention, SessionWorkingDirectoryContextWithClient? context = null, string? name = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionId); + ArgumentNullException.ThrowIfNull(sessionStatePath); + ArgumentNullException.ThrowIfNull(convention); + + var request = new SessionsCreateWorkspaceRequest { SessionId = sessionId, SessionStatePath = sessionStatePath, Convention = convention, Context = context, Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.createWorkspace", [request], cancellationToken); + } + + /// Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. + /// Root directory every session's state directory sits under. + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + /// The to monitor for cancellation requests. The default is . + /// The workspace record on disk, omitted when the session has none. + internal async Task LoadWorkspaceAsync(string sessionsHome, string sessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionsHome); + ArgumentNullException.ThrowIfNull(sessionId); + + var request = new SessionsLoadWorkspaceRequest { SessionsHome = sessionsHome, SessionId = sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.loadWorkspace", [request], cancellationToken); + } + + /// Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. + /// Root directory every session's state directory sits under. + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + /// The to monitor for cancellation requests. The default is . + /// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. + internal async Task UpdateWorkspaceFieldsAsync(string sessionsHome, string sessionId, string fieldsJson, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionsHome); + ArgumentNullException.ThrowIfNull(sessionId); + ArgumentNullException.ThrowIfNull(fieldsJson); + + var request = new SessionsUpdateWorkspaceFieldsRequest { SessionsHome = sessionsHome, SessionId = sessionId, FieldsJson = fieldsJson }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.updateWorkspaceFields", [request], cancellationToken); + } + /// Reloads user, plugin, and (optionally) repo hooks on the active session. /// Active session ID to reload hooks for. /// When true, skip repo-level hooks. Use before folder trust is confirmed; loadDeferredRepoHooks loads them post-trust. @@ -42601,35 +45289,55 @@ public async Task SpawnAsync(string cwd, string? agent } } -/// Provides server-scoped Accounts APIs. +/// Provides server-scoped Connectors APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerAccountsApi +public sealed class ServerConnectorsApi { private readonly JsonRpc _rpc; - internal ServerAccountsApi(JsonRpc rpc) + internal ServerConnectorsApi(JsonRpc rpc) { _rpc = rpc; } - /// Acquire a Microsoft Entra access token through the runtime's OneAuth broker. Account-scoped because it uses the same native broker as the account stack: a trusted host application mints a scoped Entra token for its own use, most notably to authenticate to a remote MCP server whose authorization server is Entra ID (in place of the generic browser-OAuth flow). - /// Public client application id. - /// Tenant id or tenant selector, such as common or organizations. - /// Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. - /// Exact delegated scopes to request. - /// Whether the broker may show interaction. - /// Previously rejected token that OneAuth must bypass during renewal. + /// Returns feature availability. + /// The to monitor for cancellation requests. The default is . + /// Feature availability. + public async Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getCapabilities", [], cancellationToken); + } + + /// Returns eligible accounts. + /// The to monitor for cancellation requests. The default is . + /// Eligible accounts. + public async Task GetAccountsAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.getAccounts", [], cancellationToken); + } + + /// Lists entries for the selected account. + /// Opaque account ID. + /// The to monitor for cancellation requests. The default is . + /// Entries for the selected account. + public async Task ListAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.list", [request], cancellationToken); + } + + /// Refreshes entries for the selected account. + /// Opaque account ID. /// The to monitor for cancellation requests. The default is . - /// Result of a OneAuth token acquisition. - internal async Task AcquireEntraTokenAsync(string clientId, string tenantId, string redirectUri, IList scopes, EntraTokenInteraction interaction, string? accessTokenToRenew = null, CancellationToken cancellationToken = default) + /// Entries for the selected account. + public async Task RefreshAsync(string accountId, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(clientId); - ArgumentNullException.ThrowIfNull(tenantId); - ArgumentNullException.ThrowIfNull(redirectUri); - ArgumentNullException.ThrowIfNull(scopes); + ArgumentNullException.ThrowIfNull(accountId); - var request = new EntraTokenAcquireRequest { ClientId = clientId, TenantId = tenantId, RedirectUri = redirectUri, Scopes = scopes, Interaction = interaction, AccessTokenToRenew = accessTokenToRenew }; - return await CopilotClient.InvokeRpcAsync(_rpc, "accounts.acquireEntraToken", [request], cancellationToken); + var request = new ConnectorDiscoveryAccountRequest { AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "connectors.refresh", [request], cancellationToken); } } @@ -42645,6 +45353,12 @@ internal SessionRpc(CopilotSession session) internal CopilotSession Session => _session; + /// Providers APIs. + public ProvidersApi Providers => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Sandbox APIs. public SandboxApi Sandbox => field ?? @@ -43062,6 +45776,102 @@ public async Task LogAsync(string message, SessionLogLevel? level = n } } +/// Provides session-scoped Providers APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProvidersApi +{ + private readonly CopilotSession _session; + + internal ProvidersApi(CopilotSession session) + { + _session = session; + } + + /// Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. + /// The to monitor for cancellation requests. The default is . + /// Normalized model-provider adapter definitions available to the session, not discovered instances. + public async Task GetCatalogAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionProvidersGetCatalogRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.getCatalog", [request], cancellationToken); + } + + /// Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + /// Opaque adapter identity returned by `session.providers.getCatalog`. + /// Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + /// The to monitor for cancellation requests. The default is . + /// Provider instances found by a discovery operation. + public async Task DiscoverAsync(string adapterId, object? input = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(adapterId); + _session.ThrowIfDisposed(); + + var request = new ModelProviderDiscoverRequest { SessionId = _session.SessionId, AdapterId = adapterId, Input = CopilotClient.ToJsonElementForWire(input) }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.discover", [request], cancellationToken); + } + + /// Gets current health and version information for a discovered model-provider instance. + /// Provider instance reference returned by discovery. + /// The to monitor for cancellation requests. The default is . + /// Current health information for a provider instance. + public async Task GetStatusAsync(ModelProviderInstanceReference instance, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + _session.ThrowIfDisposed(); + + var request = new ModelProviderGetStatusRequest { SessionId = _session.SessionId, Instance = instance }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.getStatus", [request], cancellationToken); + } + + /// Models APIs. + public ProvidersModelsApi Models => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; +} + +/// Provides session-scoped ProvidersModels APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProvidersModelsApi +{ + private readonly CopilotSession _session; + + internal ProvidersModelsApi(CopilotSession session) + { + _session = session; + } + + /// Lists models installed or otherwise available from a discovered model-provider instance. + /// Provider instance reference returned by discovery. + /// The to monitor for cancellation requests. The default is . + /// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + public async Task ListAsync(ModelProviderInstanceReference instance, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + _session.ThrowIfDisposed(); + + var request = new ModelProviderModelsListRequest { SessionId = _session.SessionId, Instance = instance }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.models.list", [request], cancellationToken); + } + + /// Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. + /// The discovered instance that serves the model. + /// The discovered model to configure. + /// The to monitor for cancellation requests. The default is . + /// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + public async Task PrepareConfigurationAsync(ModelProviderInstance instance, DiscoveredModel model, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(model); + _session.ThrowIfDisposed(); + + var request = new ModelProviderPrepareConfigurationRequest { SessionId = _session.SessionId, Instance = instance, Model = model }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.providers.models.prepareConfiguration", [request], cancellationToken); + } +} + /// Provides session-scoped Sandbox APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SandboxApi @@ -44417,14 +47227,15 @@ public async Task GetSourcesAsync(CancellationToke return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.getSources", [request], cancellationToken); } - /// Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk. + /// For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. /// The to monitor for cancellation requests. The default is . - public async Task ReloadAsync(CancellationToken cancellationToken = default) + /// Instruction sources loaded for the session, in merge order. + public async Task ReloadAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); var request = new SessionInstructionsReloadRequest { SessionId = _session.SessionId }; - await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.reload", [request], cancellationToken); + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.instructions.reload", [request], cancellationToken); } } @@ -44439,15 +47250,15 @@ internal CustomizationsApi(CopilotSession session) _session = session; } - /// Reloads all repository and user customizations for the active session: instructions, plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns diagnostics from the final skill reload. + /// For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. /// The to monitor for cancellation requests. The default is . - /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. - public async Task ReloadAsync(CancellationToken cancellationToken = default) + /// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. + public async Task ReloadAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); var request = new SessionCustomizationsReloadRequest { SessionId = _session.SessionId }; - return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.customizations.reload", [request], cancellationToken); + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.customizations.reload", [request], cancellationToken); } } @@ -44840,7 +47651,18 @@ internal McpApi(CopilotSession session) _session = session; } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + /// The to monitor for cancellation requests. The default is . + internal async Task SetConnectedIdeInfoAsync(SessionConnectedIdeInfo? ide = null, CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpSetConnectedIdeInfoParams { SessionId = _session.SessionId, Ide = ide }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.setConnectedIdeInfo", [request], cancellationToken); + } + + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// The to monitor for cancellation requests. The default is . /// MCP servers configured for the session, with their connection status and host-level state. public async Task ListAsync(CancellationToken cancellationToken = default) @@ -44851,6 +47673,17 @@ public async Task ListAsync(CancellationToken cancellationToken = return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.list", [request], cancellationToken); } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// The to monitor for cancellation requests. The default is . + /// Effective MCP configuration with optional live observations from matching already materialized servers. + public async Task ListConfiguredAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpListConfiguredRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.listConfigured", [request], cancellationToken); + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// Name of the connected MCP server whose tools to list. /// The to monitor for cancellation requests. The default is . @@ -45144,6 +47977,12 @@ public async Task IsServerRunningAsync(string serverNa field ?? Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + + /// Prompts APIs. + public McpPromptsApi Prompts => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; } /// Provides session-scoped McpOauth APIs. @@ -45223,7 +48062,7 @@ public async Task LoginAsync(string serverName, bool? force } /// Starts OAuth authentication for a remote MCP server. Owned servers require the original one-use prepareLogin handle and exact installation ID; manual servers retain the existing direct login behaviour. - /// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + /// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. /// The to monitor for cancellation requests. The default is . /// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. public async Task LoginAsync(McpOauthLoginRequest request, CancellationToken cancellationToken = default) @@ -45231,10 +48070,24 @@ public async Task LoginAsync(McpOauthLoginRequest request, ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request.ServerName); _session.ThrowIfDisposed(); - var wireRequest = new McpOauthLoginRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, ForceReauth = request.ForceReauth, ClientName = request.ClientName, CallbackSuccessMessage = request.CallbackSuccessMessage, ClientId = request.ClientId, ClientSecret = request.ClientSecret, PublicClient = request.PublicClient, GrantType = request.GrantType, LoginId = request.LoginId, ExpectedInstallationId = request.ExpectedInstallationId }; + var wireRequest = new McpOauthLoginRequestWithSession { SessionId = _session.SessionId, ServerName = request.ServerName, ForceReauth = request.ForceReauth, ClientName = request.ClientName, CallbackSuccessMessage = request.CallbackSuccessMessage, ClientId = request.ClientId, ClientSecret = request.ClientSecret, PublicClient = request.PublicClient, GrantType = request.GrantType, RedirectUri = request.RedirectUri, LoginId = request.LoginId, ExpectedInstallationId = request.ExpectedInstallationId }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.login", [wireRequest], cancellationToken); } + /// Completes a runtime-managed MCP OAuth login after the authorization server redirects to a host-managed callback URL. + /// Opaque identifier returned by session.mcp.oauth.login for the pending external callback. + /// Full externally visible HTTPS callback URL received by the host, including the authorization response query parameters. Applications behind a reverse proxy must reconstruct the public URL rather than passing an internal proxy URL. + /// The to monitor for cancellation requests. The default is . + public async Task CompleteAsync(string authorizationId, string callbackUrl, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(authorizationId); + ArgumentNullException.ThrowIfNull(callbackUrl); + _session.ThrowIfDisposed(); + + var request = new McpOauthCompleteRequest { SessionId = _session.SessionId, AuthorizationId = authorizationId, CallbackUrl = callbackUrl }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.oauth.complete", [request], cancellationToken); + } + /// Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. /// Name of the configured remote MCP server to probe. /// The to monitor for cancellation requests. The default is . @@ -45468,6 +48321,48 @@ public async Task ListTemplatesAsync(string ser } } +/// Provides session-scoped McpPrompts APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpPromptsApi +{ + private readonly CopilotSession _session; + + internal McpPromptsApi(CopilotSession session) + { + _session = session; + } + + /// Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// Name of the MCP server whose prompts to enumerate. + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + /// The to monitor for cancellation requests. The default is . + /// One page of prompts advertised by the named MCP server. + public async Task ListAsync(string serverName, string? cursor = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + _session.ThrowIfDisposed(); + + var request = new McpPromptsListRequest { SessionId = _session.SessionId, ServerName = serverName, Cursor = cursor }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.prompts.list", [request], cancellationToken); + } + + /// Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch referenced resources. + /// Name of the MCP server hosting the prompt. + /// The programmatic name of the prompt. + /// String-valued arguments to pass to the prompt. + /// The to monitor for cancellation requests. The default is . + /// Prompt messages returned by the MCP server without sending them to the model. + public async Task GetAsync(string serverName, string promptName, IDictionary? arguments = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + ArgumentNullException.ThrowIfNull(promptName); + _session.ThrowIfDisposed(); + + var request = new McpPromptsGetRequest { SessionId = _session.SessionId, ServerName = serverName, PromptName = promptName, Arguments = arguments }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.prompts.get", [request], cancellationToken); + } +} + /// Provides session-scoped Diagnostics APIs. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class DiagnosticsApi @@ -45531,6 +48426,17 @@ public async Task GetCapabilitiesAsync(CancellationToken return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.getCapabilities", [request], cancellationToken); } + /// Returns the session account selection, or null. + /// The to monitor for cancellation requests. The default is . + /// Session account selection, or null. + public async Task GetAccountAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionConnectorsGetAccountRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.getAccount", [request], cancellationToken); + } + /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. /// The to monitor for cancellation requests. The default is . /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. @@ -45639,10 +48545,23 @@ public async Task ReconcileAsync(string accountId, bool? refres ArgumentNullException.ThrowIfNull(accountId); _session.ThrowIfDisposed(); - var request = new ConnectorReconcileRequest { SessionId = _session.SessionId, AccountId = accountId, RefreshCatalog = refreshCatalog }; + var request = new ConnectorReconcileRequestWithSession { SessionId = _session.SessionId, AccountId = accountId, RefreshCatalog = refreshCatalog }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.reconcile", [request], cancellationToken); } + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. + /// Requests authoritative Connector-to-MCP reconciliation for the pinned account. + /// The to monitor for cancellation requests. The default is . + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + public async Task ReconcileAsync(ConnectorReconcileRequest request, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.AccountId); + _session.ThrowIfDisposed(); + var wireRequest = new ConnectorReconcileRequestWithSession { SessionId = _session.SessionId, AccountId = request.AccountId, RefreshCatalog = request.RefreshCatalog, ForceConnectorName = request.ForceConnectorName }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.reconcile", [wireRequest], cancellationToken); + } + /// Reconciles the authoritative Connector catalog into the session MCP projection during startup with a bounded deadline and fail-closed cleanup. /// Opaque account selection ID previously returned by an account discovery API. /// The to monitor for cancellation requests. The default is . @@ -46437,6 +49356,21 @@ public async Task HandlePendingElicitationAsync(string requ return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingElicitation", [request], cancellationToken); } + /// Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. + /// The unique request ID from the elicitation.requested event. + /// The elicitation response (accept with form values, decline, or cancel). + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + internal async Task HandleHumanAskUserAsync(string requestId, UIElicitationResponse result, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(result); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingElicitationRequest { SessionId = _session.SessionId, RequestId = requestId, Result = result }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanAskUser", [request], cancellationToken); + } + /// Resolves a pending `user_input.requested` event with the user's response. /// The unique request ID from the user_input.requested event. /// User response for a pending user-input request, with answer text and whether it was typed freeform. @@ -46452,6 +49386,21 @@ public async Task HandlePendingUserInputAsync(string requ return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingUserInput", [request], cancellationToken); } + /// Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + /// The unique request ID from the user_input.requested event. + /// User response for a pending user-input request, with answer text and whether it was typed freeform. + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the pending UI request was resolved by this call. + internal async Task HandleHumanUserInputAsync(string requestId, UIUserInputResponse response, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(response); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingUserInputRequest { SessionId = _session.SessionId, RequestId = requestId, Response = response }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanUserInput", [request], cancellationToken); + } + /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. /// The unique request ID from the sampling.requested event. /// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. @@ -46510,6 +49459,21 @@ public async Task HandlePendingExitPlanModeAsync(string r return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handlePendingExitPlanMode", [request], cancellationToken); } + /// Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + /// The unique request ID from the exit_plan_mode.requested event. + /// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the pending UI request was resolved by this call. + internal async Task HandleHumanExitPlanModeAsync(string requestId, UIExitPlanModeResponse response, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(response); + _session.ThrowIfDisposed(); + + var request = new UIHandlePendingExitPlanModeRequest { SessionId = _session.SessionId, RequestId = requestId, Response = response }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.ui.handleHumanExitPlanMode", [request], cancellationToken); + } + /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. /// The to monitor for cancellation requests. The default is . /// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). @@ -46965,8 +49929,8 @@ public async Task ActivityAsync(CancellationToken cancellationT } /// Returns the token breakdown for the session's current context window for a given model. - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. /// The to monitor for cancellation requests. The default is . /// Token breakdown for the session's current context window, or null if uninitialized. @@ -47113,12 +50077,12 @@ internal ShellApi(CopilotSession session) _session = session; } - /// Starts a shell command and streams output through session notifications. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. + /// Starts a shell command, returning an RPC error if it cannot be spawned. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. /// Shell command to execute. /// Working directory (defaults to session working directory). /// Timeout in milliseconds (default: 30000). /// The to monitor for cancellation requests. The default is . - /// Identifier of the spawned process, used to correlate streamed output and exit notifications. + /// Identifier of the spawned shell process, usable with shell.kill while the process is running. public async Task ExecAsync(string command, string? cwd = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); @@ -47936,11 +50900,21 @@ public interface ISessionFsHandler /// The to monitor for cancellation requests. The default is . /// File content as a UTF-8 string, or a filesystem error if the read failed. Task ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken = default); + /// Reads binary file content from the client-provided session filesystem. + /// Path of the binary file to read from the client-provided session filesystem. + /// The to monitor for cancellation requests. The default is . + /// File bytes as standard base64, or a filesystem error if the read failed. + Task ReadFileBytesAsync(SessionFsReadFileBytesRequest request, CancellationToken cancellationToken = default); /// Writes a file in the client-provided session filesystem. /// File path, content to write, and optional mode for the client-provided session filesystem. /// The to monitor for cancellation requests. The default is . /// Describes a filesystem error. Task WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken = default); + /// Writes binary file content to the client-provided session filesystem. + /// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + /// The to monitor for cancellation requests. The default is . + /// Describes a filesystem error. + Task WriteFileBytesAsync(SessionFsWriteFileBytesRequest request, CancellationToken cancellationToken = default); /// Appends content to a file in the client-provided session filesystem, creating parent directories as needed. /// File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed. /// The to monitor for cancellation requests. The default is . @@ -48077,12 +51051,24 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).SessionFs; + if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); + return await handler.ReadFileBytesAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.writeFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); return await handler.WriteFileAsync(request, cancellationToken); }), singleObjectParam: true); + rpc.SetLocalRpcMethod("sessionFs.writeFileBytes", (Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).SessionFs; + if (handler is null) throw new InvalidOperationException($"No sessionFs handler registered for session: {request.SessionId}"); + return await handler.WriteFileBytesAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.appendFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; @@ -48487,6 +51473,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.HookProgressEvent), TypeInfoPropertyName = "SessionEventsHookProgressEvent")] [JsonSerializable(typeof(GitHub.Copilot.HookStartData), TypeInfoPropertyName = "SessionEventsHookStartData")] [JsonSerializable(typeof(GitHub.Copilot.HookStartEvent), TypeInfoPropertyName = "SessionEventsHookStartEvent")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseActor), TypeInfoPropertyName = "SessionEventsHumanResponseActor")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedData), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedData")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedEvent), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.HumanResponseRecordedResponse), TypeInfoPropertyName = "SessionEventsHumanResponseRecordedResponse")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchDisabledReason), TypeInfoPropertyName = "SessionEventsIndexedSearchDisabledReason")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchErrorType), TypeInfoPropertyName = "SessionEventsIndexedSearchErrorType")] [JsonSerializable(typeof(GitHub.Copilot.IndexedSearchIncrementalPhase), TypeInfoPropertyName = "SessionEventsIndexedSearchIncrementalPhase")] @@ -48722,6 +51712,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteData), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteData")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteError), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteError")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteEvent), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteFileEdit), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteFileEdit")] +[JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteFileEditKind), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteFileEditKind")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteResult), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteResult")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteShellExecution), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteShellExecution")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionCompleteToolDescription), TypeInfoPropertyName = "SessionEventsToolExecutionCompleteToolDescription")] @@ -48749,6 +51741,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartToolDescriptionMetaUIVisibility), TypeInfoPropertyName = "SessionEventsToolExecutionStartToolDescriptionMetaUIVisibility")] [JsonSerializable(typeof(GitHub.Copilot.ToolSearchActivatedData), TypeInfoPropertyName = "SessionEventsToolSearchActivatedData")] [JsonSerializable(typeof(GitHub.Copilot.ToolSearchActivatedEvent), TypeInfoPropertyName = "SessionEventsToolSearchActivatedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputData), TypeInfoPropertyName = "SessionEventsToolShellOutputData")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputEvent), TypeInfoPropertyName = "SessionEventsToolShellOutputEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolShellOutputStream), TypeInfoPropertyName = "SessionEventsToolShellOutputStream")] [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedData), TypeInfoPropertyName = "SessionEventsToolUserRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.ToolUserRequestedEvent), TypeInfoPropertyName = "SessionEventsToolUserRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.UIEphemeralQueryPhase), TypeInfoPropertyName = "SessionEventsUIEphemeralQueryPhase")] @@ -48812,12 +51807,23 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AgentSelectRequest))] [JsonSerializable(typeof(AgentSelectResult))] [JsonSerializable(typeof(AgentSetPromptRequest))] +[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionParams))] +[JsonSerializable(typeof(AgentsCustomAgentInitialModelDecisionResult))] [JsonSerializable(typeof(AgentsDiscoverRequest))] +[JsonSerializable(typeof(AgentsGetAvailableBuiltinsRequest))] +[JsonSerializable(typeof(AgentsGetAvailableBuiltinsResult))] +[JsonSerializable(typeof(AgentsGetBuiltinDefinitionRequest))] +[JsonSerializable(typeof(AgentsGetBuiltinDefinitionResult))] +[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionRequest))] +[JsonSerializable(typeof(AgentsGetBuiltinListingDefinitionResult))] +[JsonSerializable(typeof(AgentsGetBuiltinsResult))] [JsonSerializable(typeof(AgentsGetDiscoveryPathsRequest))] [JsonSerializable(typeof(AuthEnumerateQuery))] [JsonSerializable(typeof(AuthEnumerateValue))] [JsonSerializable(typeof(AuthIdentity))] +[JsonSerializable(typeof(AuthIdentityMetadata))] [JsonSerializable(typeof(AuthInfo))] +[JsonSerializable(typeof(AuthLoginAccount))] [JsonSerializable(typeof(AuthLoginAdvanceRequest))] [JsonSerializable(typeof(AuthLoginBeginRequest))] [JsonSerializable(typeof(AuthLoginBegun))] @@ -48835,6 +51841,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AutopilotObjectiveState))] [JsonSerializable(typeof(BuiltInModelCatalog))] [JsonSerializable(typeof(BuiltInModelCatalogEntry))] +[JsonSerializable(typeof(BuiltinAgentSummary))] [JsonSerializable(typeof(BuiltinToolDescriptor))] [JsonSerializable(typeof(BuiltinToolFormat))] [JsonSerializable(typeof(BuiltinToolInputSchema))] @@ -48903,8 +51910,17 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ConnectorConnectResult))] [JsonSerializable(typeof(ConnectorContinueRequest))] [JsonSerializable(typeof(ConnectorDisconnectResult))] +[JsonSerializable(typeof(ConnectorDiscoveryAccount))] +[JsonSerializable(typeof(ConnectorDiscoveryAccountList))] +[JsonSerializable(typeof(ConnectorDiscoveryAccountRequest))] +[JsonSerializable(typeof(ConnectorDiscoveryAuthInfo))] +[JsonSerializable(typeof(ConnectorDiscoveryCapabilities))] +[JsonSerializable(typeof(ConnectorDiscoveryCatalogEntry))] +[JsonSerializable(typeof(ConnectorDiscoveryCatalogResult))] [JsonSerializable(typeof(ConnectorReconcileRequest))] +[JsonSerializable(typeof(ConnectorReconcileRequestWithSession))] [JsonSerializable(typeof(ConnectorRuntimeStatus))] +[JsonSerializable(typeof(ConnectorSessionAccount))] [JsonSerializable(typeof(ConnectorStatus))] [JsonSerializable(typeof(ContentExclusionCheckPathsRequest))] [JsonSerializable(typeof(ContentExclusionCheckPathsResult))] @@ -48920,6 +51936,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(CopilotUserResponseQuotaSnapshotsPremiumInteractions))] [JsonSerializable(typeof(CurrentModel))] [JsonSerializable(typeof(CurrentToolMetadata))] +[JsonSerializable(typeof(CustomizationReloadOutcome))] +[JsonSerializable(typeof(CustomizationsReloadResult))] [JsonSerializable(typeof(DebugCollectLogsCollectedEntry))] [JsonSerializable(typeof(DebugCollectLogsDestination))] [JsonSerializable(typeof(DebugCollectLogsEntry))] @@ -48941,10 +51959,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(DiscoveredExtensionsEnableRequest))] [JsonSerializable(typeof(DiscoveredHook))] [JsonSerializable(typeof(DiscoveredMcpServer))] +[JsonSerializable(typeof(DiscoveredModel))] +[JsonSerializable(typeof(DiscoveredModelList))] [JsonSerializable(typeof(EnqueueCommandParams))] [JsonSerializable(typeof(EnqueueCommandResult))] -[JsonSerializable(typeof(EntraTokenAcquireRequest))] -[JsonSerializable(typeof(EntraTokenAcquireResult))] [JsonSerializable(typeof(EnvironmentCapabilities))] [JsonSerializable(typeof(EnvironmentsDeleteRequest))] [JsonSerializable(typeof(EnvironmentsDeleteResult))] @@ -48973,12 +51991,29 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(FolderTrustAddParams))] [JsonSerializable(typeof(FolderTrustCheckParams))] [JsonSerializable(typeof(FolderTrustCheckResult))] +[JsonSerializable(typeof(GitCurrentBranchRemoteResult))] +[JsonSerializable(typeof(GitCwdRequest))] [JsonSerializable(typeof(GitHubEnvironment))] +[JsonSerializable(typeof(GitHubOwnerOption))] +[JsonSerializable(typeof(GitHubOwnersCancelRequest))] +[JsonSerializable(typeof(GitHubOwnersCancelResult))] +[JsonSerializable(typeof(GitHubOwnersListRequest))] +[JsonSerializable(typeof(GitHubOwnersListResult))] +[JsonSerializable(typeof(GitHubOwnersRequestIdResult))] +[JsonSerializable(typeof(GitHubRepositoryAtPathRequest))] +[JsonSerializable(typeof(GitHubRepositoryAtPathResult))] +[JsonSerializable(typeof(GitHubRepositoryIdentity))] [JsonSerializable(typeof(GitHubTelemetryClientInfo))] [JsonSerializable(typeof(GitHubTelemetryEvent))] [JsonSerializable(typeof(GitHubTelemetryNotification))] [JsonSerializable(typeof(GitHubTokenAcquireRequest))] [JsonSerializable(typeof(GitHubTokenAcquireResult))] +[JsonSerializable(typeof(GitRemoteRepository))] +[JsonSerializable(typeof(GitReposFromRemotesRequest))] +[JsonSerializable(typeof(GitReposFromRemotesResult))] +[JsonSerializable(typeof(GlobalStateLoadForConfigDirRequest))] +[JsonSerializable(typeof(GlobalStateLoadResult))] +[JsonSerializable(typeof(GlobalStateWriteKeyRequest))] [JsonSerializable(typeof(HandlePendingToolCallRequest))] [JsonSerializable(typeof(HandlePendingToolCallResult))] [JsonSerializable(typeof(HistoryAbortManualCompactionResult))] @@ -49052,6 +52087,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(LocalSessionMetadataValue))] [JsonSerializable(typeof(LogRequest))] [JsonSerializable(typeof(LogResult))] +[JsonSerializable(typeof(LoggedInUser))] [JsonSerializable(typeof(LspInitializeRequest))] [JsonSerializable(typeof(ManagedSettingMeta))] [JsonSerializable(typeof(ManagedSettingsComposeLayer))] @@ -49103,6 +52139,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpConfigUpdateRequest))] [JsonSerializable(typeof(McpConfigureGitHubRequest))] [JsonSerializable(typeof(McpConfigureGitHubResult))] +[JsonSerializable(typeof(McpConfiguredServer))] +[JsonSerializable(typeof(McpConfiguredServerList))] +[JsonSerializable(typeof(McpConfiguredServerState))] [JsonSerializable(typeof(McpDiagnosticDetails))] [JsonSerializable(typeof(McpDiagnosticSourceConfiguration))] [JsonSerializable(typeof(McpDisableRequest))] @@ -49138,6 +52177,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpListToolsRequest))] [JsonSerializable(typeof(McpListToolsResult))] [JsonSerializable(typeof(McpOauthAuthenticationStateChangedRequest))] +[JsonSerializable(typeof(McpOauthCompleteRequest))] [JsonSerializable(typeof(McpOauthHandlePendingRequest))] [JsonSerializable(typeof(McpOauthHandlePendingResult))] [JsonSerializable(typeof(McpOauthLoginRequest))] @@ -49163,6 +52203,14 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpPlanUninstallRequest))] [JsonSerializable(typeof(McpPrepareInstallRequest))] [JsonSerializable(typeof(McpPreparedInstall))] +[JsonSerializable(typeof(McpPrompt))] +[JsonSerializable(typeof(McpPromptArgument))] +[JsonSerializable(typeof(McpPromptIcon))] +[JsonSerializable(typeof(McpPromptMessage))] +[JsonSerializable(typeof(McpPromptsGetRequest))] +[JsonSerializable(typeof(McpPromptsGetResult))] +[JsonSerializable(typeof(McpPromptsListRequest))] +[JsonSerializable(typeof(McpPromptsListResult))] [JsonSerializable(typeof(McpRegisterExternalClientRequest))] [JsonSerializable(typeof(McpReloadWithConfigRequest))] [JsonSerializable(typeof(McpRemoveGitHubResult))] @@ -49225,6 +52273,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModeSetResult))] [JsonSerializable(typeof(Model))] [JsonSerializable(typeof(ModelApplyStartupOverlayRequest))] +[JsonSerializable(typeof(ModelArtifactDetails))] [JsonSerializable(typeof(ModelBilling))] [JsonSerializable(typeof(ModelBillingPromo))] [JsonSerializable(typeof(ModelBillingTokenPrices))] @@ -49243,8 +52292,25 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModelPickerSettingsContext))] [JsonSerializable(typeof(ModelPickerSettingsContextEnvironment))] [JsonSerializable(typeof(ModelPolicy))] +[JsonSerializable(typeof(ModelProviderAdapterCatalog))] +[JsonSerializable(typeof(ModelProviderAdapterDescriptor))] +[JsonSerializable(typeof(ModelProviderAdapterOperationDescriptor))] +[JsonSerializable(typeof(ModelProviderAttribution))] +[JsonSerializable(typeof(ModelProviderAutomaticDiscoveryPolicy))] +[JsonSerializable(typeof(ModelProviderConfigurationPlan))] [JsonSerializable(typeof(ModelProviderDescriptor))] +[JsonSerializable(typeof(ModelProviderDiscoverRequest))] +[JsonSerializable(typeof(ModelProviderDiscoverResult))] +[JsonSerializable(typeof(ModelProviderGetStatusRequest))] +[JsonSerializable(typeof(ModelProviderInstance))] +[JsonSerializable(typeof(ModelProviderInstanceReference))] +[JsonSerializable(typeof(ModelProviderModelsListRequest))] +[JsonSerializable(typeof(ModelProviderOperationOutcome))] +[JsonSerializable(typeof(ModelProviderPrepareConfigurationRequest))] +[JsonSerializable(typeof(ModelProviderProvenance))] [JsonSerializable(typeof(ModelProviderRef))] +[JsonSerializable(typeof(ModelProviderStatus))] +[JsonSerializable(typeof(ModelProviderWarning))] [JsonSerializable(typeof(ModelSetAllowedModelsRequest))] [JsonSerializable(typeof(ModelSetAllowedModelsResult))] [JsonSerializable(typeof(ModelSetReasoningEffortRequest))] @@ -49442,6 +52508,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SandboxHostCapability))] [JsonSerializable(typeof(SandboxHostSupport))] [JsonSerializable(typeof(SandboxMaskedEnvVar))] +[JsonSerializable(typeof(SandboxProxyCaCreateResult))] +[JsonSerializable(typeof(SandboxProxyCaRequest))] +[JsonSerializable(typeof(SandboxProxyCaStatus))] [JsonSerializable(typeof(ScheduleAddAtRequest))] [JsonSerializable(typeof(ScheduleAddCronRequest))] [JsonSerializable(typeof(ScheduleAddRequest))] @@ -49485,6 +52554,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionCommandsListRequestWithSession))] [JsonSerializable(typeof(SessionCompletionItem))] [JsonSerializable(typeof(SessionCompletionsGetTriggerCharactersRequest))] +[JsonSerializable(typeof(SessionConnectedIdeInfo))] +[JsonSerializable(typeof(SessionConnectorsGetAccountRequest))] [JsonSerializable(typeof(SessionConnectorsGetCapabilitiesRequest))] [JsonSerializable(typeof(SessionConnectorsGetStatusRequest))] [JsonSerializable(typeof(SessionConnectorsWithdrawProjectionRequest))] @@ -49499,6 +52570,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionFsExistsRequest))] [JsonSerializable(typeof(SessionFsExistsResult))] [JsonSerializable(typeof(SessionFsMkdirRequest))] +[JsonSerializable(typeof(SessionFsReadFileBytesRequest))] +[JsonSerializable(typeof(SessionFsReadFileBytesResult))] [JsonSerializable(typeof(SessionFsReadFileRequest))] [JsonSerializable(typeof(SessionFsReadFileResult))] [JsonSerializable(typeof(SessionFsReaddirRequest))] @@ -49521,6 +52594,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionFsSqliteTransactionStatement))] [JsonSerializable(typeof(SessionFsStatRequest))] [JsonSerializable(typeof(SessionFsStatResult))] +[JsonSerializable(typeof(SessionFsWriteFileBytesRequest))] [JsonSerializable(typeof(SessionFsWriteFileRequest))] [JsonSerializable(typeof(SessionGitHubAuthGetAllAuthAvailableRequest))] [JsonSerializable(typeof(SessionGitHubAuthGetCurrentAuthInfoRequest))] @@ -49549,6 +52623,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionLoadDeferredRepoHooksResult))] [JsonSerializable(typeof(SessionManagedSettingsGetRequest))] [JsonSerializable(typeof(SessionMcpAppsGetHostContextRequest))] +[JsonSerializable(typeof(SessionMcpListConfiguredRequest))] [JsonSerializable(typeof(SessionMcpListRequest))] [JsonSerializable(typeof(SessionMcpMoveLoadingToBackgroundRequest))] [JsonSerializable(typeof(SessionMcpOauthCancelLoginRequest))] @@ -49557,6 +52632,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionMcpOauthPrepareLoginResult))] [JsonSerializable(typeof(SessionMcpReloadRequest))] [JsonSerializable(typeof(SessionMcpRemoveGitHubRequest))] +[JsonSerializable(typeof(SessionMcpSetConnectedIdeInfoParams))] [JsonSerializable(typeof(SessionMetadataActivityRequest))] [JsonSerializable(typeof(SessionMetadataGetClientMetadataRequest))] [JsonSerializable(typeof(SessionMetadataGetContextAttributionRequest))] @@ -49587,6 +52663,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionPluginsReloadRequestWithSession))] [JsonSerializable(typeof(SessionProviderGetEndpointRequest))] [JsonSerializable(typeof(SessionProviderGetEndpointRequestWithSession))] +[JsonSerializable(typeof(SessionProvidersGetCatalogRequest))] [JsonSerializable(typeof(SessionPruneResult))] [JsonSerializable(typeof(SessionQueueClearRequest))] [JsonSerializable(typeof(SessionQueueEnqueueResumePendingRequest))] @@ -49634,6 +52711,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionVisibilityGetRequest))] [JsonSerializable(typeof(SessionWorkflowPauseAtCheckpointResult))] [JsonSerializable(typeof(SessionWorkingDirectoryContext))] +[JsonSerializable(typeof(SessionWorkingDirectoryContextWithClient))] [JsonSerializable(typeof(SessionWorkspacesAutopilotObjectiveExistsRequest))] [JsonSerializable(typeof(SessionWorkspacesDeleteAutopilotObjectiveRequest))] [JsonSerializable(typeof(SessionWorkspacesGetWorkspaceRequest))] @@ -49646,6 +52724,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsClientMetadataEntry))] [JsonSerializable(typeof(SessionsCloseRequest))] [JsonSerializable(typeof(SessionsCloseResult))] +[JsonSerializable(typeof(SessionsCreateWorkspaceRequest))] +[JsonSerializable(typeof(SessionsCreateWorkspaceResult))] [JsonSerializable(typeof(SessionsDeleteRequest))] [JsonSerializable(typeof(SessionsEnrichMetadataRequest))] [JsonSerializable(typeof(SessionsFindByPrefixRequest))] @@ -49669,6 +52749,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsListNonEmptySessionIdsResult))] [JsonSerializable(typeof(SessionsListRequest))] [JsonSerializable(typeof(SessionsLoadDeferredRepoHooksRequest))] +[JsonSerializable(typeof(SessionsLoadWorkspaceRequest))] +[JsonSerializable(typeof(SessionsLoadWorkspaceResult))] [JsonSerializable(typeof(SessionsOpenProgress))] [JsonSerializable(typeof(SessionsPruneOldRequest))] [JsonSerializable(typeof(SessionsReadPersistedEventsRequest))] @@ -49684,6 +52766,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsStartRemoteControlRequest))] [JsonSerializable(typeof(SessionsStopRemoteControlRequest))] [JsonSerializable(typeof(SessionsTransferRemoteControlRequest))] +[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsRequest))] +[JsonSerializable(typeof(SessionsUpdateWorkspaceFieldsResult))] [JsonSerializable(typeof(SettableAuthInfo))] [JsonSerializable(typeof(ShellCancelUserRequestedRequest))] [JsonSerializable(typeof(ShellCredentials))] @@ -49816,7 +52900,6 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(UserSettingMetadata))] [JsonSerializable(typeof(UserSettingsGetResult))] [JsonSerializable(typeof(UserSettingsSetRequest))] -[JsonSerializable(typeof(UserSettingsSetResult))] [JsonSerializable(typeof(VisibilityGetResult))] [JsonSerializable(typeof(VisibilitySetRequest))] [JsonSerializable(typeof(VisibilitySetResult))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 11c9635f42..48e214df95 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -14,6 +14,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace GitHub.Copilot; @@ -60,6 +61,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(HookEndEvent), "hook.end")] [JsonDerivedType(typeof(HookProgressEvent), "hook.progress")] [JsonDerivedType(typeof(HookStartEvent), "hook.start")] +[JsonDerivedType(typeof(HumanResponseRecordedEvent), "human_response.recorded")] [JsonDerivedType(typeof(McpAppToolCallCompleteEvent), "mcp_app.tool_call_complete")] [JsonDerivedType(typeof(McpHeadersRefreshCompletedEvent), "mcp.headers_refresh_completed")] [JsonDerivedType(typeof(McpHeadersRefreshRequiredEvent), "mcp.headers_refresh_required")] @@ -166,6 +168,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(ToolExecutionPartialResultEvent), "tool.execution_partial_result")] [JsonDerivedType(typeof(ToolExecutionProgressEvent), "tool.execution_progress")] [JsonDerivedType(typeof(ToolExecutionStartEvent), "tool.execution_start")] +[JsonDerivedType(typeof(ToolShellOutputEvent), "tool.shell_output")] [JsonDerivedType(typeof(ToolUserRequestedEvent), "tool.user_requested")] [JsonDerivedType(typeof(UiEphemeralQueryEvent), "ui.ephemeral_query")] [JsonDerivedType(typeof(UserInputCompletedEvent), "user_input.completed")] @@ -206,7 +209,7 @@ public partial class SessionEvent /// Deserializes a JSON string into a . public static SessionEvent FromJson(string json) => - JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!; + SessionEventJsonConverter.Deserialize(json); /// Serializes this event to a JSON string. public string ToJson() => @@ -216,6 +219,519 @@ public string ToJson() => private string DebuggerDisplay => ToJson(); } +internal sealed partial class SessionEventJsonConverter : JsonConverter +{ + internal static SessionEventJsonConverter Default { get; } = new(); + + public override SessionEvent? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + // Preserve the reader's position for deserialization. + var probe = reader; + JsonTypeInfo typeInfo = ReadEventTypeInfo(ref probe); + return ToSessionEvent(JsonSerializer.Deserialize(ref reader, typeInfo)); + } + + private static JsonTypeInfo? GetEventTypeInfo(ReadOnlySpan type) + { + switch (type.Length) + { + case 5: + if (type.SequenceEqual("abort"u8)) + return SessionEventsJsonContext.Default.AbortEvent; + break; + case 8: + if (type.SequenceEqual("hook.end"u8)) + return SessionEventsJsonContext.Default.HookEndEvent; + break; + case 10: + if (type.SequenceEqual("hook.start"u8)) + return SessionEventsJsonContext.Default.HookStartEvent; + break; + case 12: + if (type.SequenceEqual("session.idle"u8)) + return SessionEventsJsonContext.Default.SessionIdleEvent; + if (type.SequenceEqual("session.info"u8)) + return SessionEventsJsonContext.Default.SessionInfoEvent; + if (type.SequenceEqual("user.message"u8)) + return SessionEventsJsonContext.Default.UserMessageEvent; + break; + case 13: + if (type.SequenceEqual("hook.progress"u8)) + return SessionEventsJsonContext.Default.HookProgressEvent; + if (type.SequenceEqual("session.error"u8)) + return SessionEventsJsonContext.Default.SessionErrorEvent; + if (type.SequenceEqual("session.start"u8)) + return SessionEventsJsonContext.Default.SessionStartEvent; + if (type.SequenceEqual("skill.invoked"u8)) + return SessionEventsJsonContext.Default.SkillInvokedEvent; + break; + case 14: + if (type.SequenceEqual("assistant.idle"u8)) + return SessionEventsJsonContext.Default.AssistantIdleEvent; + if (type.SequenceEqual("command.queued"u8)) + return SessionEventsJsonContext.Default.CommandQueuedEvent; + if (type.SequenceEqual("session.resume"u8)) + return SessionEventsJsonContext.Default.SessionResumeEvent; + if (type.SequenceEqual("system.message"u8)) + return SessionEventsJsonContext.Default.SystemMessageEvent; + break; + case 15: + if (type.SequenceEqual("assistant.usage"u8)) + return SessionEventsJsonContext.Default.AssistantUsageEvent; + if (type.SequenceEqual("command.execute"u8)) + return SessionEventsJsonContext.Default.CommandExecuteEvent; + if (type.SequenceEqual("session.handoff"u8)) + return SessionEventsJsonContext.Default.SessionHandoffEvent; + if (type.SequenceEqual("session.warning"u8)) + return SessionEventsJsonContext.Default.SessionWarningEvent; + if (type.SequenceEqual("subagent.failed"u8)) + return SessionEventsJsonContext.Default.SubagentFailedEvent; + break; + case 16: + if (type.SequenceEqual("assistant.intent"u8)) + return SessionEventsJsonContext.Default.AssistantIntentEvent; + if (type.SequenceEqual("commands.changed"u8)) + return SessionEventsJsonContext.Default.CommandsChangedEvent; + if (type.SequenceEqual("model.call_start"u8)) + return SessionEventsJsonContext.Default.ModelCallStartEvent; + if (type.SequenceEqual("sandbox.decision"u8)) + return SessionEventsJsonContext.Default.SandboxDecisionEvent; + if (type.SequenceEqual("session.shutdown"u8)) + return SessionEventsJsonContext.Default.SessionShutdownEvent; + if (type.SequenceEqual("subagent.started"u8)) + return SessionEventsJsonContext.Default.SubagentStartedEvent; + break; + case 17: + if (type.SequenceEqual("agent.interrupted"u8)) + return SessionEventsJsonContext.Default.AgentInterruptedEvent; + if (type.SequenceEqual("assistant.message"u8)) + return SessionEventsJsonContext.Default.AssistantMessageEvent; + if (type.SequenceEqual("command.completed"u8)) + return SessionEventsJsonContext.Default.CommandCompletedEvent; + if (type.SequenceEqual("skill.invoked_ref"u8)) + return SessionEventsJsonContext.Default.SkillInvokedRefEvent; + if (type.SequenceEqual("subagent.selected"u8)) + return SessionEventsJsonContext.Default.SubagentSelectedEvent; + if (type.SequenceEqual("tool.shell_output"u8)) + return SessionEventsJsonContext.Default.ToolShellOutputEvent; + break; + case 18: + if (type.SequenceEqual("assistant.turn_end"u8)) + return SessionEventsJsonContext.Default.AssistantTurnEndEvent; + if (type.SequenceEqual("mcp.oauth_required"u8)) + return SessionEventsJsonContext.Default.McpOauthRequiredEvent; + if (type.SequenceEqual("model.call_failure"u8)) + return SessionEventsJsonContext.Default.ModelCallFailureEvent; + if (type.SequenceEqual("prompt_cache_break"u8)) + return SessionEventsJsonContext.Default.PromptCacheBreakEvent; + if (type.SequenceEqual("sampling.completed"u8)) + return SessionEventsJsonContext.Default.SamplingCompletedEvent; + if (type.SequenceEqual("sampling.requested"u8)) + return SessionEventsJsonContext.Default.SamplingRequestedEvent; + if (type.SequenceEqual("session.truncation"u8)) + return SessionEventsJsonContext.Default.SessionTruncationEvent; + if (type.SequenceEqual("session.usage_info"u8)) + return SessionEventsJsonContext.Default.SessionUsageInfoEvent; + if (type.SequenceEqual("subagent.completed"u8)) + return SessionEventsJsonContext.Default.SubagentCompletedEvent; + if (type.SequenceEqual("ui.ephemeral_query"u8)) + return SessionEventsJsonContext.Default.UiEphemeralQueryEvent; + break; + case 19: + if (type.SequenceEqual("assistant.reasoning"u8)) + return SessionEventsJsonContext.Default.AssistantReasoningEvent; + if (type.SequenceEqual("mcp.oauth_completed"u8)) + return SessionEventsJsonContext.Default.McpOauthCompletedEvent; + if (type.SequenceEqual("model.call_finished"u8)) + return SessionEventsJsonContext.Default.ModelCallFinishedEvent; + if (type.SequenceEqual("subagent.configured"u8)) + return SessionEventsJsonContext.Default.SubagentConfiguredEvent; + if (type.SequenceEqual("subagent.deselected"u8)) + return SessionEventsJsonContext.Default.SubagentDeselectedEvent; + if (type.SequenceEqual("system.notification"u8)) + return SessionEventsJsonContext.Default.SystemNotificationEvent; + if (type.SequenceEqual("tool.user_requested"u8)) + return SessionEventsJsonContext.Default.ToolUserRequestedEvent; + break; + case 20: + if (type.SequenceEqual("assistant.turn_retry"u8)) + return SessionEventsJsonContext.Default.AssistantTurnRetryEvent; + if (type.SequenceEqual("assistant.turn_start"u8)) + return SessionEventsJsonContext.Default.AssistantTurnStartEvent; + if (type.SequenceEqual("capabilities.changed"u8)) + return SessionEventsJsonContext.Default.CapabilitiesChangedEvent; + if (type.SequenceEqual("permission.completed"u8)) + return SessionEventsJsonContext.Default.PermissionCompletedEvent; + if (type.SequenceEqual("permission.requested"u8)) + return SessionEventsJsonContext.Default.PermissionRequestedEvent; + if (type.SequenceEqual("session.binary_asset"u8)) + return SessionEventsJsonContext.Default.SessionBinaryAssetEvent; + if (type.SequenceEqual("session.mode_changed"u8)) + return SessionEventsJsonContext.Default.SessionModeChangedEvent; + if (type.SequenceEqual("session.model_change"u8)) + return SessionEventsJsonContext.Default.SessionModelChangeEvent; + if (type.SequenceEqual("session.plan_changed"u8)) + return SessionEventsJsonContext.Default.SessionPlanChangedEvent; + if (type.SequenceEqual("tool.execution_start"u8)) + return SessionEventsJsonContext.Default.ToolExecutionStartEvent; + if (type.SequenceEqual("user_input.completed"u8)) + return SessionEventsJsonContext.Default.UserInputCompletedEvent; + if (type.SequenceEqual("user_input.requested"u8)) + return SessionEventsJsonContext.Default.UserInputRequestedEvent; + if (type.SequenceEqual("workflow.run_settled"u8)) + return SessionEventsJsonContext.Default.WorkflowRunSettledEvent; + if (type.SequenceEqual("workflow.run_started"u8)) + return SessionEventsJsonContext.Default.WorkflowRunStartedEvent; + if (type.SequenceEqual("workflow.run_updated"u8)) + return SessionEventsJsonContext.Default.WorkflowRunUpdatedEvent; + break; + case 21: + if (type.SequenceEqual("elicitation.completed"u8)) + return SessionEventsJsonContext.Default.ElicitationCompletedEvent; + if (type.SequenceEqual("elicitation.requested"u8)) + return SessionEventsJsonContext.Default.ElicitationRequestedEvent; + if (type.SequenceEqual("session.canvas.closed"u8)) + return SessionEventsJsonContext.Default.SessionCanvasClosedEvent; + if (type.SequenceEqual("session.canvas.opened"u8)) + return SessionEventsJsonContext.Default.SessionCanvasOpenedEvent; + if (type.SequenceEqual("session.skills_loaded"u8)) + return SessionEventsJsonContext.Default.SessionSkillsLoadedEvent; + if (type.SequenceEqual("session.task_complete"u8)) + return SessionEventsJsonContext.Default.SessionTaskCompleteEvent; + if (type.SequenceEqual("session.title_changed"u8)) + return SessionEventsJsonContext.Default.SessionTitleChangedEvent; + if (type.SequenceEqual("session.todos_changed"u8)) + return SessionEventsJsonContext.Default.SessionTodosChangedEvent; + if (type.SequenceEqual("session.tools_updated"u8)) + return SessionEventsJsonContext.Default.SessionToolsUpdatedEvent; + if (type.SequenceEqual("tool_search.activated"u8)) + return SessionEventsJsonContext.Default.ToolSearchActivatedEvent; + break; + case 22: + if (type.SequenceEqual("mcp.tools.list_changed"u8)) + return SessionEventsJsonContext.Default.McpToolsListChangedEvent; + if (type.SequenceEqual("session.canvas.removed"u8)) + return SessionEventsJsonContext.Default.SessionCanvasRemovedEvent; + if (type.SequenceEqual("session.indexed_search"u8)) + return SessionEventsJsonContext.Default.SessionIndexedSearchEvent; + break; + case 23: + if (type.SequenceEqual("assistant.message_delta"u8)) + return SessionEventsJsonContext.Default.AssistantMessageDeltaEvent; + if (type.SequenceEqual("assistant.message_start"u8)) + return SessionEventsJsonContext.Default.AssistantMessageStartEvent; + if (type.SequenceEqual("external_tool.completed"u8)) + return SessionEventsJsonContext.Default.ExternalToolCompletedEvent; + if (type.SequenceEqual("external_tool.requested"u8)) + return SessionEventsJsonContext.Default.ExternalToolRequestedEvent; + if (type.SequenceEqual("human_response.recorded"u8)) + return SessionEventsJsonContext.Default.HumanResponseRecordedEvent; + if (type.SequenceEqual("model.call_final_result"u8)) + return SessionEventsJsonContext.Default.ModelCallFinalResultEvent; + if (type.SequenceEqual("session.canvas.recorded"u8)) + return SessionEventsJsonContext.Default.SessionCanvasRecordedEvent; + if (type.SequenceEqual("session.context_changed"u8)) + return SessionEventsJsonContext.Default.SessionContextChangedEvent; + if (type.SequenceEqual("session.context_cleared"u8)) + return SessionEventsJsonContext.Default.SessionContextClearedEvent; + if (type.SequenceEqual("session.fusion_resolved"u8)) + return SessionEventsJsonContext.Default.SessionFusionResolvedEvent; + if (type.SequenceEqual("session.snapshot_rewind"u8)) + return SessionEventsJsonContext.Default.SessionSnapshotRewindEvent; + if (type.SequenceEqual("skill.context_delivered"u8)) + return SessionEventsJsonContext.Default.SkillContextDeliveredEvent; + if (type.SequenceEqual("tool.execution_complete"u8)) + return SessionEventsJsonContext.Default.ToolExecutionCompleteEvent; + if (type.SequenceEqual("tool.execution_progress"u8)) + return SessionEventsJsonContext.Default.ToolExecutionProgressEvent; + break; + case 24: + if (type.SequenceEqual("exit_plan_mode.completed"u8)) + return SessionEventsJsonContext.Default.ExitPlanModeCompletedEvent; + if (type.SequenceEqual("exit_plan_mode.requested"u8)) + return SessionEventsJsonContext.Default.ExitPlanModeRequestedEvent; + if (type.SequenceEqual("mcp.prompts.list_changed"u8)) + return SessionEventsJsonContext.Default.McpPromptsListChangedEvent; + if (type.SequenceEqual("session.compaction_start"u8)) + return SessionEventsJsonContext.Default.SessionCompactionStartEvent; + if (type.SequenceEqual("session.fusion_completed"u8)) + return SessionEventsJsonContext.Default.SessionFusionCompletedEvent; + if (type.SequenceEqual("session.model_deselected"u8)) + return SessionEventsJsonContext.Default.SessionModelDeselectedEvent; + if (type.SequenceEqual("session.schedule_created"u8)) + return SessionEventsJsonContext.Default.SessionScheduleCreatedEvent; + if (type.SequenceEqual("session.schedule_rearmed"u8)) + return SessionEventsJsonContext.Default.SessionScheduleRearmedEvent; + if (type.SequenceEqual("session.usage_checkpoint"u8)) + return SessionEventsJsonContext.Default.SessionUsageCheckpointEvent; + break; + case 25: + if (type.SequenceEqual("assistant.reasoning_delta"u8)) + return SessionEventsJsonContext.Default.AssistantReasoningDeltaEvent; + if (type.SequenceEqual("assistant.streaming_delta"u8)) + return SessionEventsJsonContext.Default.AssistantStreamingDeltaEvent; + if (type.SequenceEqual("assistant.tool_call_delta"u8)) + return SessionEventsJsonContext.Default.AssistantToolCallDeltaEvent; + if (type.SequenceEqual("pending_messages.modified"u8)) + return SessionEventsJsonContext.Default.PendingMessagesModifiedEvent; + if (type.SequenceEqual("permission.assentDetected"u8)) + return SessionEventsJsonContext.Default.PermissionAssentDetectedEvent; + if (type.SequenceEqual("permission.carriedForward"u8)) + return SessionEventsJsonContext.Default.PermissionCarriedForwardEvent; + if (type.SequenceEqual("session.extensions_loaded"u8)) + return SessionEventsJsonContext.Default.SessionExtensionsLoadedEvent; + break; + case 26: + if (type.SequenceEqual("auto_mode_switch.completed"u8)) + return SessionEventsJsonContext.Default.AutoModeSwitchCompletedEvent; + if (type.SequenceEqual("auto_mode_switch.requested"u8)) + return SessionEventsJsonContext.Default.AutoModeSwitchRequestedEvent; + if (type.SequenceEqual("mcp_app.tool_call_complete"u8)) + return SessionEventsJsonContext.Default.McpAppToolCallCompleteEvent; + if (type.SequenceEqual("mcp.resources.list_changed"u8)) + return SessionEventsJsonContext.Default.McpResourcesListChangedEvent; + if (type.SequenceEqual("session.auto_mode_resolved"u8)) + return SessionEventsJsonContext.Default.SessionAutoModeResolvedEvent; + if (type.SequenceEqual("session.canvas.unavailable"u8)) + return SessionEventsJsonContext.Default.SessionCanvasUnavailableEvent; + if (type.SequenceEqual("session.completion_receipt"u8)) + return SessionEventsJsonContext.Default.SessionCompletionReceiptEvent; + if (type.SequenceEqual("session.mcp_server_removed"u8)) + return SessionEventsJsonContext.Default.SessionMcpServerRemovedEvent; + if (type.SequenceEqual("session.mcp_servers_loaded"u8)) + return SessionEventsJsonContext.Default.SessionMcpServersLoadedEvent; + if (type.SequenceEqual("session.schedule_cancelled"u8)) + return SessionEventsJsonContext.Default.SessionScheduleCancelledEvent; + break; + case 27: + if (type.SequenceEqual("session.compaction_complete"u8)) + return SessionEventsJsonContext.Default.SessionCompactionCompleteEvent; + if (type.SequenceEqual("session.custom_notification"u8)) + return SessionEventsJsonContext.Default.SessionCustomNotificationEvent; + if (type.SequenceEqual("session.fusion_route_failed"u8)) + return SessionEventsJsonContext.Default.SessionFusionRouteFailedEvent; + if (type.SequenceEqual("session.permission_recovery"u8)) + return SessionEventsJsonContext.Default.SessionPermissionRecoveryEvent; + if (type.SequenceEqual("session.permissions_changed"u8)) + return SessionEventsJsonContext.Default.SessionPermissionsChangedEvent; + if (type.SequenceEqual("skill.context_delivered_ref"u8)) + return SessionEventsJsonContext.Default.SkillContextDeliveredRefEvent; + break; + case 28: + if (type.SequenceEqual("mcp.headers_refresh_required"u8)) + return SessionEventsJsonContext.Default.McpHeadersRefreshRequiredEvent; + if (type.SequenceEqual("session.fusion_route_started"u8)) + return SessionEventsJsonContext.Default.SessionFusionRouteStartedEvent; + break; + case 29: + if (type.SequenceEqual("assistant.fusion_phase_failed"u8)) + return SessionEventsJsonContext.Default.AssistantFusionPhaseFailedEvent; + if (type.SequenceEqual("mcp.headers_refresh_completed"u8)) + return SessionEventsJsonContext.Default.McpHeadersRefreshCompletedEvent; + if (type.SequenceEqual("session.custom_agents_updated"u8)) + return SessionEventsJsonContext.Default.SessionCustomAgentsUpdatedEvent; + if (type.SequenceEqual("session.mode_notice_delivered"u8)) + return SessionEventsJsonContext.Default.SessionModeNoticeDeliveredEvent; + if (type.SequenceEqual("tool.execution_partial_result"u8)) + return SessionEventsJsonContext.Default.ToolExecutionPartialResultEvent; + break; + case 30: + if (type.SequenceEqual("assistant.fusion_phase_started"u8)) + return SessionEventsJsonContext.Default.AssistantFusionPhaseStartedEvent; + if (type.SequenceEqual("assistant.server_tool_progress"u8)) + return SessionEventsJsonContext.Default.AssistantServerToolProgressEvent; + if (type.SequenceEqual("session.session_limits_changed"u8)) + return SessionEventsJsonContext.Default.SessionSessionLimitsChangedEvent; + if (type.SequenceEqual("session.workspace_file_changed"u8)) + return SessionEventsJsonContext.Default.SessionWorkspaceFileChangedEvent; + break; + case 31: + if (type.SequenceEqual("assistant.fusion_phase_activity"u8)) + return SessionEventsJsonContext.Default.AssistantFusionPhaseActivityEvent; + if (type.SequenceEqual("permission.messageAuthorization"u8)) + return SessionEventsJsonContext.Default.PermissionMessageAuthorizationEvent; + if (type.SequenceEqual("session.auto_tier_switch_failed"u8)) + return SessionEventsJsonContext.Default.SessionAutoTierSwitchFailedEvent; + if (type.SequenceEqual("session.canvas.registry_changed"u8)) + return SessionEventsJsonContext.Default.SessionCanvasRegistryChangedEvent; + break; + case 32: + if (type.SequenceEqual("assistant.fusion_phase_completed"u8)) + return SessionEventsJsonContext.Default.AssistantFusionPhaseCompletedEvent; + if (type.SequenceEqual("session.auto_tier_recommendation"u8)) + return SessionEventsJsonContext.Default.SessionAutoTierRecommendationEvent; + if (type.SequenceEqual("session.background_tasks_changed"u8)) + return SessionEventsJsonContext.Default.SessionBackgroundTasksChangedEvent; + if (type.SequenceEqual("session.remote_steerable_changed"u8)) + return SessionEventsJsonContext.Default.SessionRemoteSteerableChangedEvent; + break; + case 33: + if (type.SequenceEqual("session.managed_settings_enforced"u8)) + return SessionEventsJsonContext.Default.SessionManagedSettingsEnforcedEvent; + if (type.SequenceEqual("session.managed_settings_resolved"u8)) + return SessionEventsJsonContext.Default.SessionManagedSettingsResolvedEvent; + if (type.SequenceEqual("session.mcp_server_status_changed"u8)) + return SessionEventsJsonContext.Default.SessionMcpServerStatusChangedEvent; + break; + case 34: + if (type.SequenceEqual("permission.contextualAuthorization"u8)) + return SessionEventsJsonContext.Default.PermissionContextualAuthorizationEvent; + if (type.SequenceEqual("session_limits_exhausted.completed"u8)) + return SessionEventsJsonContext.Default.SessionLimitsExhaustedCompletedEvent; + if (type.SequenceEqual("session_limits_exhausted.requested"u8)) + return SessionEventsJsonContext.Default.SessionLimitsExhaustedRequestedEvent; + if (type.SequenceEqual("session.mcp_server_needs_reconnect"u8)) + return SessionEventsJsonContext.Default.SessionMcpServerNeedsReconnectEvent; + break; + case 35: + if (type.SequenceEqual("permission.messageAuthorizationRead"u8)) + return SessionEventsJsonContext.Default.PermissionMessageAuthorizationReadEvent; + if (type.SequenceEqual("session.autopilot_objective_changed"u8)) + return SessionEventsJsonContext.Default.SessionAutopilotObjectiveChangedEvent; + break; + case 37: + if (type.SequenceEqual("session.extensions.attachments_pushed"u8)) + return SessionEventsJsonContext.Default.SessionExtensionsAttachmentsPushedEvent; + break; + case 39: + if (type.SequenceEqual("permission.messageAuthorizationDegraded"u8)) + return SessionEventsJsonContext.Default.PermissionMessageAuthorizationDegradedEvent; + break; + } + return null; + } + + public override void Write( + Utf8JsonWriter writer, + SessionEvent value, + JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, SessionEventsJsonContext.Default.SessionEvent); + + private static SessionEvent ToSessionEvent(object? value) => + value as SessionEvent ?? ((SessionEventEnvelope?)value)?.ToSessionEvent()!; + + private static JsonTypeInfo ReadEventTypeInfo(ref Utf8JsonReader reader) + { + if (reader.TokenType == JsonTokenType.Null) + { + return SessionEventsJsonContext.Default.SessionEventEnvelope; + } + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected a session event object."); + } + + JsonTypeInfo? typeInfo = null; + bool foundDiscriminator = false; + while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) + { + bool isDiscriminator = reader.ValueIsEscaped + ? ReadString(ref reader) == "type" + : reader.ValueTextEquals("type"u8); + if (isDiscriminator) + { + if (foundDiscriminator) + { + throw new JsonException("Duplicate session event type discriminator."); + } + foundDiscriminator = true; + reader.Read(); + if (reader.TokenType == JsonTokenType.String) + { + if (reader.HasValueSequence || reader.ValueIsEscaped) + { + typeInfo = GetEventTypeInfo(StrictUtf8.GetBytes(ReadString(ref reader))); + } + else + { + typeInfo = GetEventTypeInfo(reader.ValueSpan); + if (typeInfo is null) + { + // Validate unknown discriminator text too, including invalid UTF-8. + _ = ReadString(ref reader); + } + } + } + else if (reader.TokenType != JsonTokenType.Number || !reader.TryGetInt32(out _)) + { + throw new JsonException("Expected a string or integer session event type discriminator."); + } + } + else + { + bool isMetadata = reader.HasValueSequence || reader.ValueIsEscaped + ? ReadString(ref reader) is { Length: > 0 } propertyName && propertyName[0] == '$' + : !reader.ValueSpan.IsEmpty && reader.ValueSpan[0] == (byte)'$'; + if (isMetadata) + { + throw new JsonException("Unexpected session event metadata property."); + } + reader.Read(); + reader.Skip(); + } + } + + return typeInfo ?? SessionEventsJsonContext.Default.SessionEventEnvelope; + } +} + +internal sealed class SessionEventJsonTypeInfoResolver : IJsonTypeInfoResolver +{ + internal static SessionEventJsonTypeInfoResolver Default { get; } = new(); + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (type != typeof(SessionEvent)) + { + return null; + } + + JsonTypeInfo typeInfo = JsonMetadataServices.CreateValueInfo( + options, + SessionEventJsonConverter.Default); + typeInfo.PolymorphismOptions = null; + return typeInfo; + } +} + +internal sealed class SessionEventEnvelope +{ + /// Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } + + /// When true, the event is transient and not persisted to the session event log on disk. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ephemeral")] + public bool? Ephemeral { get; set; } + + /// Unique event identifier (UUID v4), generated when the event is emitted. + [JsonPropertyName("id")] + public Guid Id { get; set; } + + /// ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + [JsonPropertyName("parentId")] + public Guid? ParentId { get; set; } + + /// ISO 8601 timestamp when the event was created. + [JsonPropertyName("timestamp")] + public DateTimeOffset Timestamp { get; set; } + + internal SessionEvent ToSessionEvent() => new() + { + AgentId = AgentId, + Ephemeral = Ephemeral, + Id = Id, + ParentId = ParentId, + Timestamp = Timestamp, + }; +} + /// Session initialization metadata including context and configuration. /// Represents the session.start event. public sealed partial class SessionStartEvent : SessionEvent @@ -1137,8 +1653,14 @@ public sealed partial class ToolExecutionStartEvent : SessionEvent public required ToolExecutionStartData Data { get; set; } } -/// Streaming tool execution output for incremental result display. +/// Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. /// Represents the tool.execution_partial_result event. +[EditorBrowsable(EditorBrowsableState.Never)] +#if NET5_0_OR_GREATER +[Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] +#endif public sealed partial class ToolExecutionPartialResultEvent : SessionEvent { /// @@ -1150,6 +1672,19 @@ public sealed partial class ToolExecutionPartialResultEvent : SessionEvent public required ToolExecutionPartialResultData Data { get; set; } } +/// Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +/// Represents the tool.shell_output event. +public sealed partial class ToolShellOutputEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "tool.shell_output"; + + /// The tool.shell_output event payload. + [JsonPropertyName("data")] + public required ToolShellOutputData Data { get; set; } +} + /// Tool execution progress notification with status message. /// Represents the tool.execution_progress event. public sealed partial class ToolExecutionProgressEvent : SessionEvent @@ -1892,6 +2427,19 @@ public sealed partial class ExitPlanModeCompletedEvent : SessionEvent public required ExitPlanModeCompletedData Data { get; set; } } +/// Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +/// Represents the human_response.recorded event. +public sealed partial class HumanResponseRecordedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "human_response.recorded"; + + /// The human_response.recorded event payload. + [JsonPropertyName("data")] + public required HumanResponseRecordedData Data { get; set; } +} + /// Payload of `session.tools_updated` identifying the model whose resolved tools were updated. /// Represents the session.tools_updated event. public sealed partial class SessionToolsUpdatedEvent : SessionEvent @@ -2246,6 +2794,11 @@ public sealed partial class SessionStartData [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortModel")] + public string? ReasoningEffortModel { get; set; } + /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningSummary")] @@ -2326,6 +2879,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortModel")] + public string? ReasoningEffortModel { get; set; } + /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningSummary")] @@ -2647,6 +3205,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffortModel")] + public string? ReasoningEffortModel { get; set; } + /// Reasoning summary mode after the model change, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningSummary")] @@ -4160,6 +4723,8 @@ public sealed partial class AssistantMessageData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -4249,6 +4814,8 @@ public sealed partial class AssistantMessageDeltaData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -4306,6 +4873,11 @@ public sealed partial class AssistantUsageData [JsonPropertyName("availableToolCount")] internal long? AvailableToolCount { get; set; } + /// Where the bring-your-own-key model runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("byokKind")] + public string? ByokKind { get; set; } + /// Whether the provider reported prompt-cache usage details for this call. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonInclude] @@ -4417,6 +4989,11 @@ public sealed partial class AssistantUsageData [JsonPropertyName("model")] public required string Model { get; set; } + /// Fixed-set provider family serving the bring-your-own-key model (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelProvider")] + public string? ModelProvider { get; set; } + /// Number of tool calls returned by the model. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonInclude] @@ -4438,6 +5015,8 @@ public sealed partial class AssistantUsageData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -4625,6 +5204,12 @@ public sealed partial class PromptCacheBreakData [JsonPropertyName("toolsRedefined")] internal string[]? ToolsRedefined { get; set; } + /// Changed definition parts of redefined tools, as `tool:part` entries; property-level parts only for telemetry-safe tools, whose other names are hashed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] + [JsonPropertyName("toolsRedefinedParts")] + internal string[]? ToolsRedefinedParts { get; set; } + /// Raw names of tools redefined since the prior call, restricted because a tool name can be user-authored. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonInclude] @@ -4668,6 +5253,11 @@ public sealed partial class ModelCallFailureData [JsonPropertyName("badRequestKind")] public ModelCallFailureBadRequestKind? BadRequestKind { get; set; } + /// Where the bring-your-own-key model for the failed call runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("byokKind")] + public string? ByokKind { get; set; } + /// Duration of the failed API call in milliseconds. [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -4735,6 +5325,11 @@ public sealed partial class ModelCallFailureData [JsonPropertyName("model")] public string? Model { get; set; } + /// Fixed-set provider family serving the bring-your-own-key model for the failed call (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelProvider")] + public string? ModelProvider { get; set; } + /// Parent task tool call ID when this failed model call belongs to a sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -4942,6 +5537,8 @@ public sealed partial class ToolExecutionStartData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -4981,10 +5578,16 @@ public sealed partial class ToolExecutionStartData public string? TurnId { get; set; } } -/// Streaming tool execution output for incremental result display. +/// Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. +[EditorBrowsable(EditorBrowsableState.Never)] +#if NET5_0_OR_GREATER +[Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] +#endif public sealed partial class ToolExecutionPartialResultData { - /// Incremental output chunk from the running tool. + /// Merged replacement snapshot from the running shell, not an append-only chunk. [JsonPropertyName("partialOutput")] public required string PartialOutput { get; set; } @@ -4993,6 +5596,27 @@ public sealed partial class ToolExecutionPartialResultData public required string ToolCallId { get; set; } } +/// Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +public sealed partial class ToolShellOutputData +{ + /// Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("stream")] + public ToolShellOutputStream? Stream { get; set; } + + /// New output to append, without synthetic shell-result markers or stream-switch separators. + [JsonPropertyName("text")] + public required string Text { get; set; } + + /// Tool call ID that owns this shell output. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } +} + /// Tool execution progress notification with status message. public sealed partial class ToolExecutionProgressData { @@ -5000,6 +5624,12 @@ public sealed partial class ToolExecutionProgressData [JsonPropertyName("progressMessage")] public required string ProgressMessage { get; set; } + /// Client-only structured progress metadata. Not model-facing tool output. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("structuredContent")] + public JsonElement? StructuredContent { get; set; } + /// Tool call ID this progress notification belongs to. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -5013,6 +5643,12 @@ public sealed partial class ToolExecutionCompleteData [JsonPropertyName("error")] public ToolExecutionCompleteError? Error { get; set; } + /// Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("fileEdits")] + public ToolExecutionCompleteFileEdit[]? FileEdits { get; set; } + /// Experimental HydraFusion attribution for this tool completion. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -5044,6 +5680,8 @@ public sealed partial class ToolExecutionCompleteData [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] #endif [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] @@ -6427,6 +7065,11 @@ public sealed partial class SessionAutoModeResolvedData [JsonPropertyName("routingMethod")] public string? RoutingMethod { get; set; } + /// Short human-readable sentence from the routing service explaining why this model was chosen, for display alongside the model. Present only when the service supplied one: it is omitted for on-device selections, when the service did not provide an explanation, and when a replayed decision made no routing call. The text is display-only and drawn from a fixed catalogue; several distinct routing categories share identical wording, so it cannot be used to recover the category or keyed on programmatically. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("selectionReason")] + public string? SelectionReason { get; set; } + /// Whether a sticky model choice overrode the router result. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("stickyOverride")] @@ -6587,6 +7230,27 @@ public sealed partial class ExitPlanModeCompletedData public ExitPlanModeAction? SelectedAction { get; set; } } +/// Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +public sealed partial class HumanResponseRecordedData +{ + /// Controlled actor provenance established at response ingress. + [JsonPropertyName("actor")] + public required HumanResponseActor Actor { get; set; } + + /// Request ID of the runtime-owned question or plan review. + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + /// Typed request and response payload. + [JsonPropertyName("response")] + public required HumanResponseRecordedResponse Response { get; set; } + + /// Tool call ID that opened the request, when present. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } +} + /// Payload of `session.tools_updated` identifying the model whose resolved tools were updated. public sealed partial class SessionToolsUpdatedData { @@ -6695,7 +7359,7 @@ public sealed partial class SessionMcpServersLoadedData /// Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. public sealed partial class SessionMcpServerStatusChangedData { - /// Runtime configuration provenance for a failed connection, or unknown when unavailable. Additional string values may be introduced. + /// Runtime configuration provenance for a connected or failed server, or unknown when unavailable. Additional string values may be introduced. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("configSource")] public string? ConfigSource { get; set; } @@ -8452,6 +9116,11 @@ public sealed partial class AssistantMessageReasoningBlocks [JsonPropertyName("blocks")] public JsonElement[]? Blocks { get; set; } + /// Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("orderedBlocks")] + public JsonElement[]? OrderedBlocks { get; set; } + /// Model provider that produced these reasoning blocks. [JsonPropertyName("provider")] public required string Provider { get; set; } @@ -8774,6 +9443,20 @@ public sealed partial class ToolExecutionCompleteError public RemediationAction? Remediation { get; set; } } +/// A file mutation that was actually committed by a built-in file editing tool. +/// Nested data type for ToolExecutionCompleteFileEdit. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed partial class ToolExecutionCompleteFileEdit +{ + /// Kind of mutation committed at this path. + [JsonPropertyName("kind")] + public required ToolExecutionCompleteFileEditKind Kind { get; set; } + + /// Absolute path in the session filesystem namespace. + [JsonPropertyName("path")] + public required string Path { get; set; } +} + /// Binary result returned by a tool for the model. /// Nested data type for PersistedBinaryImage. public sealed partial class PersistedBinaryImage @@ -9014,6 +9697,8 @@ public sealed partial class ToolExecutionCompleteContentText : ToolExecutionComp [EditorBrowsable(EditorBrowsableState.Never)] #if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] #endif public sealed partial class ToolExecutionCompleteContentTerminal : ToolExecutionCompleteContent { @@ -12198,6 +12883,115 @@ public sealed partial class CapabilitiesChangedUI public bool? McpApps { get; set; } } +/// The ask_user variant of . +public sealed partial class HumanResponseRecordedResponseAskUser : HumanResponseRecordedResponse +{ + /// + [JsonIgnore] + public override string ResponseKind => "ask_user"; + + /// Exact answer content accepted from the user. + [JsonPropertyName("content")] + public required IDictionary Content { get; set; } + + /// Exact question displayed to the user. + [JsonPropertyName("message")] + public required string Message { get; set; } + + /// Exact response schema displayed to the user. + [JsonPropertyName("requestedSchema")] + public required ElicitationRequestedSchema RequestedSchema { get; set; } +} + +/// The user_input variant of . +public sealed partial class HumanResponseRecordedResponseUserInput : HumanResponseRecordedResponse +{ + /// + [JsonIgnore] + public override string ResponseKind => "user_input"; + + /// Whether the displayed request allowed a free-form answer. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("allowFreeform")] + public bool? AllowFreeform { get; set; } + + /// Exact selected or free-form answer submitted by the user. + [JsonPropertyName("answer")] + public required string Answer { get; set; } + + /// Exact choices displayed to the user, when the request offered choices. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("choices")] + public string[]? Choices { get; set; } + + /// Exact question displayed to the user. + [JsonPropertyName("question")] + public required string Question { get; set; } + + /// Whether the answer was typed as free-form text rather than selected from the displayed choices. + [JsonPropertyName("wasFreeform")] + public required bool WasFreeform { get; set; } +} + +/// The exit_plan_mode variant of . +public sealed partial class HumanResponseRecordedResponseExitPlanMode : HumanResponseRecordedResponse +{ + /// + [JsonIgnore] + public override string ResponseKind => "exit_plan_mode"; + + /// Actions offered by the plan review UI. + [JsonPropertyName("actions")] + public required ExitPlanModeAction[] Actions { get; set; } + + /// Whether the user approved the reviewed plan. + [JsonPropertyName("approved")] + public required bool Approved { get; set; } + + /// Whether the selected response requested edit auto-approval. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoApproveEdits")] + public bool? AutoApproveEdits { get; set; } + + /// Exact feedback submitted with the plan decision, when present. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } + + /// Exact full plan content available from the review UI. + [JsonPropertyName("planContent")] + public required string PlanContent { get; set; } + + /// Action the plan review UI recommended. + [JsonPropertyName("recommendedAction")] + public required ExitPlanModeAction RecommendedAction { get; set; } + + /// Action selected by the user, when applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("selectedAction")] + public ExitPlanModeAction? SelectedAction { get; set; } + + /// Exact plan summary displayed to the user. + [JsonPropertyName("summary")] + public required string Summary { get; set; } +} + +/// Exact runtime-owned question or reviewed plan paired with the typed response that settled it. +/// Polymorphic base type discriminated by responseKind. +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "responseKind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(HumanResponseRecordedResponseAskUser), "ask_user")] +[JsonDerivedType(typeof(HumanResponseRecordedResponseUserInput), "user_input")] +[JsonDerivedType(typeof(HumanResponseRecordedResponseExitPlanMode), "exit_plan_mode")] +public partial class HumanResponseRecordedResponse +{ + /// The type discriminator. + [JsonPropertyName("responseKind")] + public virtual string ResponseKind { get; set; } = string.Empty; +} + + /// A single resolved skill in `session.skills_loaded`, including source, invocability, enabled state, path, and argument hint. /// Nested data type for SkillsLoadedSkill. public sealed partial class SkillsLoadedSkill @@ -16477,7 +17271,7 @@ public override void Write(Utf8JsonWriter writer, AbortReason value, JsonSeriali } } -/// Configuration source: user, workspace, plugin, builtin, or managed. +/// Configuration source: user, workspace, plugin, builtin, managed, or account. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct McpServerSource : IEquatable @@ -16511,6 +17305,9 @@ public McpServerSource(string value) /// Server supplied by a trusted host-managed catalog. public static McpServerSource Managed { get; } = new("managed"); + /// Server contributed by a signed-in account; enablement and organization policy still apply. + public static McpServerSource Account { get; } = new("account"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(McpServerSource left, McpServerSource right) => left.Equals(right); @@ -16675,6 +17472,135 @@ public override void Write(Utf8JsonWriter writer, ToolExecutionStartToolDescript } } +/// Shell output source. Terminal output has no separate stdout/stderr attribution. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ToolShellOutputStream : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ToolShellOutputStream(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Output from the shell command's standard output stream. This is the default when stream is omitted. + public static ToolShellOutputStream Stdout { get; } = new("stdout"); + + /// Output from the shell command's standard error stream. + public static ToolShellOutputStream Stderr { get; } = new("stderr"); + + /// Inherently merged output that cannot be attributed separately to stdout or stderr. + public static ToolShellOutputStream Terminal { get; } = new("terminal"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ToolShellOutputStream left, ToolShellOutputStream right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ToolShellOutputStream left, ToolShellOutputStream right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ToolShellOutputStream other && Equals(other); + + /// + public bool Equals(ToolShellOutputStream 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 ToolShellOutputStream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ToolShellOutputStream value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ToolShellOutputStream)); + } + } +} + +/// Kind of file mutation committed by a built-in editing tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ToolExecutionCompleteFileEditKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ToolExecutionCompleteFileEditKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A file was created. + public static ToolExecutionCompleteFileEditKind Create { get; } = new("create"); + + /// A file was written by an edit operation. + public static ToolExecutionCompleteFileEditKind Edit { get; } = new("edit"); + + /// A file was deleted. + public static ToolExecutionCompleteFileEditKind Delete { get; } = new("delete"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ToolExecutionCompleteFileEditKind left, ToolExecutionCompleteFileEditKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ToolExecutionCompleteFileEditKind left, ToolExecutionCompleteFileEditKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ToolExecutionCompleteFileEditKind other && Equals(other); + + /// + public bool Equals(ToolExecutionCompleteFileEditKind 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 ToolExecutionCompleteFileEditKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ToolExecutionCompleteFileEditKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ToolExecutionCompleteFileEditKind)); + } + } +} + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -18711,6 +19637,9 @@ public PermissionApprovalEvaluationReasonCode(string value) /// The script path was not authorized for inspection. public static PermissionApprovalEvaluationReasonCode PathNotAuthorized { get; } = new("path-not-authorized"); + /// A code source was excluded from review by content exclusion policy. + public static PermissionApprovalEvaluationReasonCode ContentExcluded { get; } = new("content-excluded"); + /// The script working directory was invalid. public static PermissionApprovalEvaluationReasonCode InvalidWorkingDirectory { get; } = new("invalid-working-directory"); @@ -18747,6 +19676,24 @@ public PermissionApprovalEvaluationReasonCode(string value) /// The script argument binding could not be reviewed. public static PermissionApprovalEvaluationReasonCode ArgumentBindingUnreviewable { get; } = new("argument-binding-unreviewable"); + /// The shell command could not be analyzed for execution evidence. + public static PermissionApprovalEvaluationReasonCode UnsupportedCommandShape { get; } = new("unsupported-command-shape"); + + /// The shell command used a code source that cannot be bound for review. + public static PermissionApprovalEvaluationReasonCode UnsupportedSource { get; } = new("unsupported-source"); + + /// The shell command used a code source computed at run time. + public static PermissionApprovalEvaluationReasonCode DynamicSource { get; } = new("dynamic-source"); + + /// The shell command referenced more code sources than can be reviewed. + public static PermissionApprovalEvaluationReasonCode TooManySources { get; } = new("too-many-sources"); + + /// A code-bearing executable could not be inspected. + public static PermissionApprovalEvaluationReasonCode ExecutableUnavailable { get; } = new("executable-unavailable"); + + /// A code-bearing executable exceeded the binding size limit. + public static PermissionApprovalEvaluationReasonCode ExecutableTooLarge { get; } = new("executable-too-large"); + /// The script review metadata was malformed. public static PermissionApprovalEvaluationReasonCode MalformedScriptActionReview { get; } = new("malformed-script-action-review"); @@ -20240,6 +21187,70 @@ public override void Write(Utf8JsonWriter writer, ExitPlanModeAction value, Json } } +/// Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HumanResponseActor : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HumanResponseActor(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A built-in trusted client submitted the response after direct human interaction. + public static HumanResponseActor HumanResponse { get; } = new("human_response"); + + /// A host or SDK automation submitted the response without direct human interaction. + public static HumanResponseActor HostAutomation { get; } = new("host_automation"); + + /// The response came through a legacy or otherwise unattributed ingress. + public static HumanResponseActor Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HumanResponseActor left, HumanResponseActor right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HumanResponseActor left, HumanResponseActor right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HumanResponseActor other && Equals(other); + + /// + public bool Equals(HumanResponseActor 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 HumanResponseActor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HumanResponseActor value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HumanResponseActor)); + } + } +} + /// Terminal status a workflow run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -20484,7 +21495,7 @@ public McpServerStatus(string value) /// The server is configured but disabled. public static McpServerStatus Disabled { get; } = new("disabled"); - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. public static McpServerStatus Stopped { get; } = new("stopped"); /// The server is not configured for this session. @@ -20802,6 +21813,12 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(HookProgressEvent))] [JsonSerializable(typeof(HookStartData))] [JsonSerializable(typeof(HookStartEvent))] +[JsonSerializable(typeof(HumanResponseRecordedData))] +[JsonSerializable(typeof(HumanResponseRecordedEvent))] +[JsonSerializable(typeof(HumanResponseRecordedResponse))] +[JsonSerializable(typeof(HumanResponseRecordedResponseAskUser))] +[JsonSerializable(typeof(HumanResponseRecordedResponseExitPlanMode))] +[JsonSerializable(typeof(HumanResponseRecordedResponseUserInput))] [JsonSerializable(typeof(McpAppToolCallCompleteData))] [JsonSerializable(typeof(McpAppToolCallCompleteError))] [JsonSerializable(typeof(McpAppToolCallCompleteEvent))] @@ -20962,6 +21979,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionErrorData))] [JsonSerializable(typeof(SessionErrorEvent))] [JsonSerializable(typeof(SessionEvent))] +[JsonSerializable(typeof(SessionEventEnvelope))] [JsonSerializable(typeof(SessionExtensionsAttachmentsPushedData))] [JsonSerializable(typeof(SessionExtensionsAttachmentsPushedEvent))] [JsonSerializable(typeof(SessionExtensionsLoadedData))] @@ -21116,6 +22134,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionCompleteData))] [JsonSerializable(typeof(ToolExecutionCompleteError))] [JsonSerializable(typeof(ToolExecutionCompleteEvent))] +[JsonSerializable(typeof(ToolExecutionCompleteFileEdit))] [JsonSerializable(typeof(ToolExecutionCompleteResult))] [JsonSerializable(typeof(ToolExecutionCompleteShellExecution))] [JsonSerializable(typeof(ToolExecutionCompleteToolDescription))] @@ -21142,6 +22161,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionStartToolDescriptionMetaUI))] [JsonSerializable(typeof(ToolSearchActivatedData))] [JsonSerializable(typeof(ToolSearchActivatedEvent))] +[JsonSerializable(typeof(ToolShellOutputData))] +[JsonSerializable(typeof(ToolShellOutputEvent))] [JsonSerializable(typeof(ToolUserRequestedData))] [JsonSerializable(typeof(ToolUserRequestedEvent))] [JsonSerializable(typeof(UiEphemeralQueryData))] diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index 26d4fe297a..8d45eb6f4a 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -1039,11 +1039,12 @@ private sealed class IncomingRequestCancellation : IDisposable public IncomingRequestCancellation(long id, CancellationToken connectionClosedToken) { Id = id; - _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken); _registration = _requestSource.Token.Register(static state => { ((TaskCompletionSource)state!).TrySetResult(); }, _requestCancelled); + // Cancellation callbacks run in reverse registration order: propagate to handlers before replying. + _combinedSource = CancellationTokenSource.CreateLinkedTokenSource(_requestSource.Token, connectionClosedToken); } public long Id { get; } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index ce50859571..88d00cde24 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -58,7 +58,7 @@ namespace GitHub.Copilot; /// public sealed partial class CopilotSession : IAsyncDisposable { - private readonly Dictionary _toolHandlers = []; + private IReadOnlyDictionary _toolHandlers = new Dictionary(StringComparer.Ordinal); private readonly Dictionary> _commandHandlers = []; private readonly Dictionary>> _bearerTokenProviders = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _pendingExternalTools = new(StringComparer.Ordinal); @@ -73,12 +73,14 @@ public sealed partial class CopilotSession : IAsyncDisposable private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; private volatile Func>? _autoModeSwitchHandler; + private volatile ISkillProvider? _skillProvider; private ImmutableArray _eventHandlers = ImmutableArray.Empty; private sealed record EventSubscription(Type EventType, Action Handler, bool RootAgentOnly); private SessionHooks? _hooks; private readonly SemaphoreSlim _hooksLock = new(1, 1); + private readonly SemaphoreSlim _setToolsLock = new(1, 1); private Dictionary>>? _transformCallbacks; private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1); @@ -125,6 +127,9 @@ private sealed record EventSubscription(Type EventType, Action Han /// public string? WorkspacePath { get; internal set; } + /// Details of transcript repair reported by session.resume, if any. + public TranscriptRecoveryReport? TranscriptRecovery { get; internal set; } + /// /// Gets the capabilities reported by the host for this session. /// @@ -229,6 +234,7 @@ internal void CloseEventChannel() /// internal void Unregister() { + ClearSkillProvider(); CancelPendingExternalTools(); CloseEventChannel(); RemoveFromClient(); @@ -602,14 +608,152 @@ private async Task ProcessEventsAsync() /// internal void RegisterTools(ICollection tools) { - _toolHandlers.Clear(); + Volatile.Write(ref _toolHandlers, BuildToolHandlerMap(tools)); + } + + private static Dictionary BuildToolHandlerMap(ICollection tools) + { + var handlers = new Dictionary(StringComparer.Ordinal); foreach (var tool in tools) { if (tool.GetService() is { } function) { - _toolHandlers.Add(tool.Name, function); + handlers.Add(tool.Name, function); } } + + return handlers; + } + + private static ProtocolExternalToolDefinition ToProtocolExternalToolDefinition(AIFunctionDeclaration function) + { + // Start from the create/resume definition so every path sends the same tool fields. + var definition = CopilotClient.ToolDefinition.FromAIFunction(function); + return new ProtocolExternalToolDefinition + { + Name = definition.Name, + // session.tools.set requires a description string. + Description = definition.Description ?? string.Empty, + Parameters = ToJsonElementDictionary(definition.Parameters), + OverridesBuiltInTool = definition.OverridesBuiltInTool, + SkipPermission = definition.SkipPermission, + Defer = definition.Defer switch + { + CopilotToolDefer.Auto => ProtocolExternalToolDefer.Auto, + CopilotToolDefer.Never => ProtocolExternalToolDefer.Never, + _ => (ProtocolExternalToolDefer?)null, + }, + Metadata = definition.Metadata is { } metadata ? ToJsonElementDictionary(metadata) : null, + IsTerminal = definition.IsTerminal, + }; + } + + private static Dictionary? ToJsonElementDictionary(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + { + return null; + } + + var properties = new Dictionary(StringComparer.Ordinal); + foreach (var property in element.EnumerateObject()) + { + properties.Add(property.Name, property.Value.Clone()); + } + + return properties; + } + + private static Dictionary ToJsonElementDictionary(IDictionary nodes) + { + var properties = new Dictionary(StringComparer.Ordinal); + foreach (var (name, value) in nodes) + { + using var document = JsonDocument.Parse(value?.ToJsonString() ?? "null"); + properties.Add(name, document.RootElement.Clone()); + } + + return properties; + } + + /// + /// Replaces the complete custom tool list supplied by this SDK client for the live session. + /// + /// + /// The same tool declarations accepted by when creating or resuming a session. + /// Passing an empty collection removes all externally implemented tools supplied by this client. + /// + /// + /// A token that cancels the call. Cancelling while an earlier replacement is still in flight sends nothing. Once the + /// request is sent, cancelling only stops the wait: an accepted replacement still installs its handlers. + /// + /// A task that completes after the runtime accepts the replacement. + /// + /// + /// This is a complete replacement for this client's externally implemented tools only. Built-in tools, MCP/plugin tools, + /// extension-discovered tools, subagent tools, and tools supplied by other connections are unchanged. The wire definitions + /// are the same as the tools supplied when creating or resuming a session. + /// + /// + /// Tool handlers switch when the runtime's acceptance response arrives, before subsequent tool requests are dispatched. + /// Tool calls already running finish with the handlers that started them. If the runtime rejects the replacement, the + /// previous handlers remain installed and the exception is propagated. Concurrent calls are applied in order. + /// + /// + /// The agent sees the new tools from its next model request, which can fall within a turn in progress. A model request + /// already in flight was made with the previous tools, so the agent can still call a tool you removed. This session doesn't + /// answer that call, and it can stay pending until the turn is aborted. If a running turn might still call a tool you remove, + /// replace tools while the session is idle. + /// + /// + [Experimental(Diagnostics.Experimental)] + public async Task SetToolsAsync(ICollection tools, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(tools); + ThrowIfDisposed(); + cancellationToken.ThrowIfCancellationRequested(); + + var wireTools = tools.Select(ToProtocolExternalToolDefinition).ToList(); + var handlers = BuildToolHandlerMap(tools); + + // Cancelling before sending the request leaves handlers unchanged. Once sent, the request runs + // to completion even if the caller stops waiting, so an accepted replacement still installs its handlers. + var replacement = ReplaceToolsAsync(wireTools, handlers, cancellationToken); + try + { + await replacement.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + _ = replacement.ContinueWith( + static completed => _ = completed.Exception, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); + throw; + } + } + + private async Task ReplaceToolsAsync( + List wireTools, + Dictionary handlers, + CancellationToken lockCancellationToken) + { + await _setToolsLock.WaitAsync(lockCancellationToken); + try + { + // SemaphoreSlim can grant a released slot while its cancellation continuation is pending. + lockCancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var request = new ToolsSetRequest { SessionId = SessionId, Tools = wireTools }; + await CopilotClient.InvokeRpcAsync( + JsonRpc, "session.tools.set", [request], null, CancellationToken.None, + onResponseInline: _ => Volatile.Write(ref _toolHandlers, handlers)); + } + finally + { + _setToolsLock.Release(); + } } /// @@ -619,7 +763,8 @@ internal void RegisterTools(ICollection tools) /// The tool if found; otherwise, null. internal AIFunction? GetTool(string name) { - return _toolHandlers.TryGetValue(name, out var tool) ? tool : null; + var handlers = Volatile.Read(ref _toolHandlers); + return handlers.TryGetValue(name, out var tool) ? tool : null; } /// @@ -1061,6 +1206,17 @@ await Rpc.Tools.HandlePendingToolCallAsync( static string GetSingleParameterName(AIFunction tool) { + if (tool.Name == "apply_patch" && + tool.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var isOverride) && + isOverride is true && + tool.JsonSchema.TryGetProperty("type", out var schemaType) && + schemaType.ValueKind == JsonValueKind.String && + schemaType.GetString() == "string") + { + // AIFunction uses named arguments even when the override declares a scalar schema. + return "input"; + } + if (tool.JsonSchema.TryGetProperty("properties", out var properties) && properties.ValueKind == JsonValueKind.Object) { @@ -1252,6 +1408,48 @@ internal void RegisterAutoModeSwitchHandler(Func + /// Registers the session-scoped skill provider callback. + /// + internal void RegisterSkillProvider(ISkillProvider? provider) + { + _skillProvider = provider; + } + + internal void ClearSkillProvider() => _skillProvider = null; + + internal async ValueTask HandleSkillProviderListAsync(CancellationToken cancellationToken) + { + var provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}"); + + try + { + var skills = await provider.ListSkillsAsync(cancellationToken).ConfigureAwait(false); + return new CopilotClient.SkillProviderListResult(skills?.ToList() ?? []); + } + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + { + LogSkillProviderFailed(ex, "listSkills", SessionId); + throw new InvalidOperationException("Skill provider listSkills failed", ex); + } + } + + internal async ValueTask HandleSkillProviderReadAsync(string name, CancellationToken cancellationToken) + { + var provider = _skillProvider ?? throw new InvalidOperationException($"No skill provider for session: {SessionId}"); + + try + { + var markdown = await provider.ReadSkillAsync(name, cancellationToken).ConfigureAwait(false); + return new CopilotClient.SkillProviderReadResult(markdown); + } + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + { + LogSkillProviderFailed(ex, "readSkill", SessionId); + throw new InvalidOperationException("Skill provider readSkill failed", ex); + } + } + /// /// Registers per-provider BearerTokenProvider callbacks for BYOK /// providers configured with managed-identity / on-demand bearer-token auth. @@ -1864,6 +2062,16 @@ internal void RegisterHooks(SessionHooks hooks) JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.AgentStopHookInput)!, invocation) : null, + "subagentStart" => hooks.OnSubagentStart != null + ? await hooks.OnSubagentStart( + JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.SubagentStartHookInput)!, + invocation) + : null, + "subagentStop" => hooks.OnSubagentStop != null + ? await hooks.OnSubagentStop( + JsonSerializer.Deserialize(input.GetRawText(), SessionJsonContext.Default.SubagentStopHookInput)!, + invocation) + : null, _ => null }; } @@ -2232,6 +2440,7 @@ public async ValueTask DisposeAsync() return; } + ClearSkillProvider(); CancelPendingExternalTools(); CloseEventChannel(); @@ -2260,7 +2469,7 @@ public async ValueTask DisposeAsync() } _eventHandlers = ImmutableInterlocked.InterlockedExchange(ref _eventHandlers, ImmutableArray.Empty); - _toolHandlers.Clear(); + Volatile.Write(ref _toolHandlers, new Dictionary(StringComparer.Ordinal)); _commandHandlers.Clear(); _permissionHandler = null; @@ -2268,6 +2477,7 @@ public async ValueTask DisposeAsync() _elicitationHandler = null; _exitPlanModeHandler = null; _autoModeSwitchHandler = null; + _skillProvider = null; } [LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in broadcast event handler")] @@ -2285,6 +2495,9 @@ public async ValueTask DisposeAsync() [LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")] private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId); + [LoggerMessage(Level = LogLevel.Warning, Message = "Skill provider {Operation} failed. SessionId={SessionId}")] + private partial void LogSkillProviderFailed(Exception exception, string operation, string sessionId); + internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; @@ -2370,6 +2583,10 @@ internal void ThrowIfDisposed() [JsonSerializable(typeof(SessionEndHookOutput))] [JsonSerializable(typeof(SessionStartHookInput))] [JsonSerializable(typeof(SessionStartHookOutput))] + [JsonSerializable(typeof(SubagentStartHookInput))] + [JsonSerializable(typeof(SubagentStartHookOutput))] + [JsonSerializable(typeof(SubagentStopHookInput))] + [JsonSerializable(typeof(SubagentStopHookOutput))] [JsonSerializable(typeof(SystemMessageTransformRpcResponse))] [JsonSerializable(typeof(SystemMessageTransformSection))] [JsonSerializable(typeof(Attachment))] diff --git a/dotnet/src/SessionEventJsonConverter.cs b/dotnet/src/SessionEventJsonConverter.cs new file mode 100644 index 0000000000..6b4e9a58a6 --- /dev/null +++ b/dotnet/src/SessionEventJsonConverter.cs @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Buffers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +#if !NETSTANDARD2_0 +using System.Text.Unicode; +#endif + +namespace GitHub.Copilot; + +internal sealed partial class SessionEventJsonConverter +{ + private static readonly Encoding StrictUtf8 = new UTF8Encoding(false, true); + + internal static SessionEvent Deserialize(string json) + { +#if !NETSTANDARD2_0 + const int StackallocThreshold = 512; +#endif + ArgumentNullException.ThrowIfNull(json); + + byte[]? rented = null; + Span buffer = +#if !NETSTANDARD2_0 + json.Length <= StackallocThreshold / 3 ? stackalloc byte[StackallocThreshold] : +#endif + rented = ArrayPool.Shared.Rent(GetUtf8ByteCount(json)); + try + { +#if NETSTANDARD2_0 + int byteCount = StrictUtf8.GetBytes(json, 0, json.Length, rented, 0); +#else + var status = Utf8.FromUtf16(json.AsSpan(), buffer, out _, out int byteCount, replaceInvalidSequences: false); + if (status != OperationStatus.Done) + { + throw new ArgumentException($"Cannot transcode UTF-16 JSON text to UTF-8 ({status}).", nameof(json)); + } +#endif + ReadOnlySpan utf8 = buffer.Slice(0, byteCount); + var reader = new Utf8JsonReader(utf8); + + // Select metadata without entering the converter: reader-based nested deserialization + // would scan the entire object again to scope its input before parsing it. + JsonTypeInfo typeInfo; + try + { + reader.Read(); + typeInfo = ReadEventTypeInfo(ref reader); + } + catch (JsonException ex) when (ex.GetType() != typeof(JsonException)) + { + // Match JsonSerializer's normalization of reader errors. + throw new JsonException(ex.Message, ex.Path, ex.LineNumber, ex.BytePositionInLine, ex); + } + return ToSessionEvent(JsonSerializer.Deserialize(utf8, typeInfo)); + } + finally + { + if (rented is not null) + { + buffer.Clear(); + ArrayPool.Shared.Return(rented); + } + } + } + + private static int GetUtf8ByteCount(string json) + { + try + { + return StrictUtf8.GetByteCount(json); + } + catch (EncoderFallbackException ex) + { + throw new ArgumentException("Cannot transcode invalid UTF-16 JSON text to UTF-8.", nameof(json), ex); + } + } + + private static string ReadString(ref Utf8JsonReader reader) + { + try + { + return reader.GetString()!; + } + catch (InvalidOperationException ex) + { + throw new JsonException(ex.Message, ex); + } + } +} diff --git a/dotnet/src/SessionFsProvider.cs b/dotnet/src/SessionFsProvider.cs index a353c93ad0..9aaf25f4b7 100644 --- a/dotnet/src/SessionFsProvider.cs +++ b/dotnet/src/SessionFsProvider.cs @@ -73,6 +73,22 @@ public interface ISessionFsSqliteProvider Task ExistsAsync(CancellationToken cancellationToken); } +/// Optional interface for providers that support exact binary file reads and writes. +public interface ISessionFsBinaryProvider +{ + /// Reads exact bytes of a file in the session filesystem. + /// Path to read. + /// Cancellation token. + Task ReadFileBytesAsync(string path, CancellationToken cancellationToken); + + /// Writes exact bytes of a file in the session filesystem. + /// Path to write. + /// Exact file bytes. + /// Optional POSIX-style permission mode. + /// Cancellation token. + Task WriteFileBytesAsync(string path, ReadOnlyMemory content, int? mode, CancellationToken cancellationToken); +} + /// /// Optional capability for session filesystem providers that support atomic SQLite transactions. /// @@ -119,73 +135,92 @@ public SessionFsSqliteTransactionException( public SessionFsSqliteTransactionErrorClass ErrorClass { get; } } +/// Thrown from a provider's WriteFileAsync when the failed write changed its target. +public sealed class SessionFsWriteException : Exception +{ + /// Initializes a failed write that changed its target. + /// Human-readable failure description. + /// Optional underlying exception. + public SessionFsWriteException(string message, Exception? innerException = null) + : base(message, innerException) + { + } +} + /// /// Base class for session filesystem providers. Subclasses override the /// virtual methods and use normal C# patterns (return values, throw exceptions). /// The base class catches exceptions and converts them to -/// results expected by the runtime. +/// results expected by the runtime. Existence checks instead return false if the provider throws. /// To add SQLite support, also implement . /// +/// +/// Paths use the configured and are passed to the provider unchanged. +/// Absolute paths refer to the provider's filesystem namespace, not necessarily the runtime host's filesystem. +/// public abstract class SessionFsProvider : ISessionFsHandler { + private const int MaxBinaryBytes = (64 * 1024 * 1024 - 1024) / 4 * 3; + private const int MaxBinaryContentLength = (MaxBinaryBytes + 2) / 3 * 4; + /// Reads the full content of a file. Throw if the file does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. /// The file content as a UTF-8 string. protected abstract Task ReadFileAsync(string path, CancellationToken cancellationToken); - /// Writes content to a file, creating it (and parent directories) if needed. - /// SessionFs-relative path. + /// Writes content to a file, creating it (and parent directories) if needed. Throw if a failed write changed the target. + /// Absolute or relative path in the provider's filesystem namespace. /// Content to write. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Appends content to a file, creating it (and parent directories) if needed. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Content to append. /// Optional POSIX-style permission mode. Null means use OS default. /// Cancellation token. protected abstract Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken); /// Checks whether a path exists. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. /// true if the path exists, false otherwise. protected abstract Task ExistsAsync(string path, CancellationToken cancellationToken); /// Gets metadata about a file or directory. Throw if the path does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task StatAsync(string path, CancellationToken cancellationToken); - /// Creates a directory (and optionally parents). Does not fail if it already exists. - /// SessionFs-relative path. + /// Creates a directory. If is true, creates parent directories as needed. + /// Absolute or relative path in the provider's filesystem namespace. /// Whether to create parent directories. /// Optional POSIX-style permission mode (e.g., 0x1FF for 0777). Null means use OS default. /// Cancellation token. protected abstract Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken); /// Lists entry names in a directory. Throw if the directory does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken); /// Lists entries with type info in a directory. Throw if the directory does not exist. - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken); /// Removes a file or directory. Throw if the path does not exist (unless is true). - /// SessionFs-relative path. + /// Absolute or relative path in the provider's filesystem namespace. /// Whether to remove directory contents recursively. /// If true, do not throw when the path does not exist. /// Cancellation token. protected abstract Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken); /// Renames/moves a file or directory. - /// Source path. - /// Destination path. + /// Absolute or relative source path in the provider's filesystem namespace. + /// Absolute or relative destination path in the provider's filesystem namespace. /// Cancellation token. protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken); @@ -206,6 +241,37 @@ async Task ISessionFsHandler.ReadFileAsync(SessionFsRea } } + async Task ISessionFsHandler.ReadFileBytesAsync(SessionFsReadFileBytesRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + try + { + if (this is not ISessionFsBinaryProvider provider) + { + throw new NotSupportedException("Binary reads are not supported by this provider."); + } + var bytes = await provider.ReadFileBytesAsync(request.Path, cancellationToken).ConfigureAwait(false); + if (bytes.Length > MaxBinaryBytes) + { + return new SessionFsReadFileBytesResult + { + Content = string.Empty, + Error = new SessionFsError + { + Code = SessionFsErrorCode.UNKNOWN, + Message = "sessionFs.readFileBytes content exceeds the binary read limit" + } + }; + } + return new SessionFsReadFileBytesResult { Content = Convert.ToBase64String(bytes) }; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + return new SessionFsReadFileBytesResult { Content = string.Empty, Error = ToSessionFsError(ex) }; + } + } + async Task ISessionFsHandler.WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); @@ -216,6 +282,39 @@ async Task ISessionFsHandler.ReadFileAsync(SessionFsRea return null; } catch (Exception ex) + { + var error = ToSessionFsError(ex); + if (ex is SessionFsWriteException) + { + error.WriteChanged = true; + } + return error; + } + } + + async Task ISessionFsHandler.WriteFileBytesAsync(SessionFsWriteFileBytesRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + try + { + if (this is not ISessionFsBinaryProvider provider) + { + throw new NotSupportedException("Binary writes are not supported by this provider."); + } + if (request.Content.Length > MaxBinaryContentLength) + { + return new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "sessionFs.writeFileBytes content exceeds the binary write limit" }; + } + var content = Convert.FromBase64String(request.Content); + if (Convert.ToBase64String(content) != request.Content) + { + return new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "invalid sessionFs.writeFileBytes base64 content" }; + } + await provider.WriteFileBytesAsync(request.Path, content, (int?)request.Mode, cancellationToken).ConfigureAwait(false); + return null; + } + catch (Exception ex) when (ex is not OperationCanceledException) { return ToSessionFsError(ex); } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index f40a71b95e..82a291e400 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -52,6 +52,74 @@ internal static class Diagnostics internal const string Experimental = "GHCP001"; } +/// +/// Describes a skill supplied by a session-scoped . +/// +/// +/// Experimental. The runtime validates descriptor limits, name syntax, and +/// case-insensitive uniqueness when it reads the provider catalog. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class SkillProviderDescriptor +{ + /// Stable skill name. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Human-readable description of what the skill teaches the model. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// + /// Whether users can invoke the skill explicitly. When unset, the runtime default is . + /// + [JsonPropertyName("userInvocable")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? UserInvocable { get; set; } + + /// + /// Whether model-initiated invocation is disabled. When unset, the runtime default is . + /// + [JsonPropertyName("disableModelInvocation")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DisableModelInvocation { get; set; } + + /// Optional argument hint shown for explicit invocations. + [JsonPropertyName("argumentHint")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ArgumentHint { get; set; } +} + +/// +/// Provides session-scoped skills to the Copilot runtime. +/// +/// +/// Experimental. Implementations can be called concurrently and must be safe for +/// concurrent use. The provider is not persisted; supply it again when resuming a +/// session. Returning from +/// reports that the requested skill was not found. +/// +[Experimental(Diagnostics.Experimental)] +public interface ISkillProvider +{ + /// Lists the skills currently available from this provider. + /// + /// Cancels when the runtime abandons the request (for example, on timeout, session disposal, + /// or provider replacement on resume) or the connection closes. + /// + /// The available skill descriptors. + Task> ListSkillsAsync(CancellationToken cancellationToken); + + /// Reads the SKILL.md markdown for a named skill. + /// The skill name requested by the runtime. + /// + /// Cancels when the runtime abandons the request (for example, on timeout, session disposal, + /// or provider replacement on resume) or the connection closes. + /// + /// The skill markdown, or when the skill is not found. + Task ReadSkillAsync(string name, CancellationToken cancellationToken); +} + /// /// Log level for the Copilot runtime. Use the well-known values exposed as /// static members (, , , @@ -2052,6 +2120,165 @@ public sealed class AgentStopHookOutput public string? Reason { get; set; } } +/// +/// Input for a subagent-start hook, before the sub-agent's first turn. +/// +public sealed class SubagentStartHookInput +{ + /// + /// The runtime session ID of the parent session. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Unix timestamp in milliseconds when the hook was triggered. + /// + [JsonPropertyName("timestamp")] + [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] + public DateTimeOffset Timestamp { get; set; } + + /// + /// Current working directory of the parent session. + /// + [JsonPropertyName("cwd")] + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Path to the parent session's on-disk transcript, or empty if unavailable. + /// + [JsonPropertyName("transcriptPath")] + public string TranscriptPath { get; set; } = string.Empty; + + /// + /// Name of the sub-agent being started. + /// + [JsonPropertyName("agentName")] + public string AgentName { get; set; } = string.Empty; + + /// + /// Display name of the sub-agent, when available. + /// + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } + + /// + /// Description of the sub-agent, when available. + /// + [JsonPropertyName("agentDescription")] + public string? AgentDescription { get; set; } +} + +/// +/// Output for a subagent-start hook. +/// +public sealed class SubagentStartHookOutput +{ + /// + /// Additional context prepended to the sub-agent's initial prompt. + /// + [JsonPropertyName("additionalContext")] + public string? AdditionalContext { get; set; } +} + +/// +/// Input for a subagent-stop hook, after a sub-agent turn completes. +/// +public sealed class SubagentStopHookInput +{ + /// + /// The runtime session ID of the parent session. + /// + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// + /// Unix timestamp in milliseconds when the hook was triggered. + /// + [JsonPropertyName("timestamp")] + [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))] + public DateTimeOffset Timestamp { get; set; } + + /// + /// Current working directory of the parent session. + /// + [JsonPropertyName("cwd")] + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Path to the parent session's on-disk transcript, or empty if unavailable. + /// + [JsonPropertyName("transcriptPath")] + public string TranscriptPath { get; set; } = string.Empty; + + /// + /// Name of the sub-agent that stopped. + /// + [JsonPropertyName("agentName")] + public string AgentName { get; set; } = string.Empty; + + /// + /// Display name of the sub-agent, when available. + /// + [JsonPropertyName("agentDisplayName")] + public string? AgentDisplayName { get; set; } + + /// + /// Description of the sub-agent, when available. + /// + [JsonPropertyName("agentDescription")] + public string? AgentDescription { get; set; } + + /// + /// Identifier of the sub-agent, when available. + /// + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } + + /// + /// Type of the sub-agent that stopped. + /// + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; + + /// + /// Reason the sub-agent stopped, such as "end_turn". + /// + [JsonPropertyName("stopReason")] + public string StopReason { get; set; } = string.Empty; + + /// + /// Last assistant response from the sub-agent. + /// + [JsonPropertyName("response")] + public string Response { get; set; } = string.Empty; +} + +/// +/// Output for a subagent-stop hook. +/// +public sealed class SubagentStopHookOutput +{ + /// + /// Set to "block" to request another sub-agent turn, or "allow" to accept it. + /// Other values fail the sub-agent instead of silently allowing its stop. + /// + [JsonPropertyName("decision")] + public string? Decision { get; set; } + + /// + /// Nonempty instruction required when the stop is blocked; invalid without Decision = "block". + /// + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// + /// Replacement for the sub-agent's final response when the stop is not blocked. + /// + [JsonPropertyName("modifiedResponse")] + public string? ModifiedResponse { get; set; } +} + /// /// Hook handlers configuration for a session. /// @@ -2108,6 +2335,16 @@ public sealed class SessionHooks /// Handler called when the top-level agent reaches a natural stop. /// public Func>? OnAgentStop { get; set; } + + /// + /// Handler called before a sub-agent's first turn. + /// + public Func>? OnSubagentStart { get; set; } + + /// + /// Handler called after a sub-agent turn completes. + /// + public Func>? OnSubagentStop { get; set; } } /// @@ -2284,9 +2521,9 @@ public sealed class SectionOverride public static SystemMessageSection ToolInstructions { get; } = new("tool_instructions"); /// Repository and organization custom instructions. public static SystemMessageSection CustomInstructions { get; } = new("custom_instructions"); - /// Runtime-provided context and instructions (e.g. system notifications, memories, workspace context, mode-specific instructions, content-exclusion policy). + /// Runtime-provided system-prompt context and instructions, such as system notifications, memories, workspace context, and content-exclusion policy. Mode-specific instructions can travel in transition messages instead. public static SystemMessageSection RuntimeInstructions { get; } = new("runtime_instructions"); - /// End-of-prompt instructions: parallel tool calling, persistence, task completion. + /// End-of-prompt instructions: parallel tool calling, persistence, task completion, and configured subagent-model guidance when the task tool is available. public static SystemMessageSection LastInstructions { get; } = new("last_instructions"); /// Gets the underlying string value of this . @@ -2401,6 +2638,15 @@ public sealed class ProviderConfig [JsonPropertyName("transport")] public string? Transport { get; set; } + /// + /// Product serving the model, such as ollama or lm_studio, reported in telemetry + /// as model_provider. Allowed values are openai, anthropic, + /// azure_openai, ollama, lm_studio, foundry_local, and + /// llama_cpp; only affects telemetry. + /// + [JsonPropertyName("modelProvider")] + public string? ModelProvider { get; set; } + /// /// Base URL of the provider's API endpoint. /// @@ -2571,6 +2817,15 @@ public sealed class NamedProviderConfig [JsonPropertyName("wireApi")] public string? WireApi { get; set; } + /// + /// Product serving this provider's models, such as ollama or lm_studio, reported + /// in telemetry as model_provider. Allowed values are openai, anthropic, + /// azure_openai, ollama, lm_studio, foundry_local, and + /// llama_cpp; only affects telemetry. + /// + [JsonPropertyName("modelProvider")] + public string? ModelProvider { get; set; } + /// /// API endpoint URL. /// @@ -3247,6 +3502,15 @@ public sealed class ManagedSettingsPermissions /// Tool-permission patterns that are allowed without prompting. [JsonPropertyName("allow")] public IList? Allow { get; set; } + + /// + /// Closed-world host boundary expressed as Domain(hostname), + /// Domain(IP), or Domain(*.example.com) rules. Schemes, ports, + /// paths, queries, and fragments are rejected. Multiple managed layers + /// intersect their lists. A present empty list denies all hosts. + /// + [JsonPropertyName("limitTo")] + public IList? LimitTo { get; set; } } /// @@ -3385,6 +3649,7 @@ protected SessionConfigBase(SessionConfigBase? other) ExpAssignments = other.ExpAssignments; EnableManagedSettings = other.EnableManagedSettings; ManagedSettings = other.ManagedSettings; + SkillProvider = other.SkillProvider; #pragma warning disable GHCP001 Canvases = other.Canvases is not null ? [.. other.Canvases] : null; RequestCanvasRenderer = other.RequestCanvasRenderer; @@ -3527,6 +3792,20 @@ protected SessionConfigBase(SessionConfigBase? other) /// public bool? EnableSkills { get; set; } + /// + /// Session-scoped skill provider. When set, the runtime can list and read + /// skills by calling back into this SDK host. + /// + /// + /// Experimental. The provider is ephemeral and is never persisted; supply it + /// again when resuming a session. In , + /// set to to make provider + /// skills active. Providers can be called concurrently. + /// + [JsonIgnore] + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + public ISkillProvider? SkillProvider { get; set; } + /// /// Built-in skill names to include in the session. In /// , omitting this option excludes all @@ -4044,6 +4323,15 @@ private SessionConfig(SessionConfig? other) : base(other) public SessionConfig Clone() => new(this); } +/// Details of a transcript repair performed while resuming a session. +/// Path planned for the original transcript backup. +/// Invalid or torn-tail lines affected by repair. +/// Whether the session start event was relocated. +public sealed record TranscriptRecoveryReport( + string PlannedBackupPath, + IList InvalidLineNumbers, + bool SessionStartMoved); + /// /// Configuration options for resuming an existing Copilot session. /// @@ -4062,6 +4350,7 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) SuppressResumeEvent = other.SuppressResumeEvent; ContinuePendingWork = other.ContinuePendingWork; + AllowTranscriptRecovery = other.AllowTranscriptRecovery; OpenCanvases = other.OpenCanvases is not null ? [.. other.OpenCanvases] : null; } @@ -4075,7 +4364,8 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) /// When , instructs the runtime to continue any tool calls /// or permission prompts that were still pending when the session was last suspended. /// When (the default), the runtime treats pending work as - /// interrupted on resume. + /// interrupted on resume. Completed tool results already durably recorded + /// by the runtime are preserved. /// /// For permission requests, the runtime re-emits permission.requested so the /// registered handler can re-prompt; @@ -4085,6 +4375,13 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) /// public bool? ContinuePendingWork { get; set; } + /// + /// Whether to repair a damaged transcript on resume. Defaults to true in + /// all modes. Set false to reject recovery. Recovery can discard a torn tail; + /// inspect after resume. + /// + public bool? AllowTranscriptRecovery { get; set; } + #pragma warning disable GHCP001 /// /// Snapshot of canvases that were already open when the session was suspended. @@ -4672,6 +4969,7 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(SectionOverride))] [JsonSerializable(typeof(SessionMetadata))] [JsonSerializable(typeof(SetForegroundSessionResponse))] +[JsonSerializable(typeof(SkillProviderDescriptor))] [JsonSerializable(typeof(SystemMessageConfig))] [JsonSerializable(typeof(ToolBinaryResult))] [JsonSerializable(typeof(ToolBinaryResultType))] diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 528901d290..1775fb1159 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -58,7 +58,6 @@ COPILOT_CLI_DOWNLOAD_BASE_URL is also honored. --> $(COPILOT_CLI_DOWNLOAD_BASE_URL) - https://github.com/github/copilot-cli/releases/download + +```go +type memorySkillProvider struct{} + +func (memorySkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + return []rpc.SkillProviderDescriptor{{ + Name: "review", + Description: "Review code using the host application's policy", + }}, nil +} + +func (memorySkillProvider) ReadSkill(ctx context.Context, name string) (string, error) { + if name != "review" { + return "", copilot.ErrSkillNotFound + } + return "Review the changes and call out policy violations.", nil +} + +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SkillProvider: memorySkillProvider{}, + // Required only when ClientOptions.Mode is ModeEmpty. + // EnableSkills: copilot.Bool(true), +}) +``` + +`ReadSkill` returns an error for which +`errors.Is(err, copilot.ErrSkillNotFound)` is true when a listed skill no +longer exists. Other errors are reported to the runtime as generic provider +failures so provider error text is not exposed to the model. + +Skill providers are not supported for cloud sessions. Creating a cloud session +with a provider fails before the client connects with: +`Skill providers are not supported for cloud sessions.` + +Provider methods can be called concurrently by the runtime, including from +sub-agents that inherit the root session binding. Implementations must be safe +for concurrent calls and should observe the `context.Context`. The context is +cancelled when the runtime abandons the request: the call times out, the +session disconnects or is deleted, or a resume replaces the provider. It is not +cancelled when the connection closes or the client is force-stopped. + ## Installation confirmation (experimental) Set `ClientOptions.InstallationConfirmationHandler` to receive the runtime's @@ -309,6 +359,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `UseLoggedInUser` (\*bool): Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `URIConnection`. - `EnableRemoteSessions` (bool): Enable remote session support (Mission Control integration). Ignored with `URIConnection`. - `Telemetry` (\*TelemetryConfig): OpenTelemetry configuration for the runtime. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `SessionFS` (\*SessionFSConfig): Custom session filesystem provider configuration. For the `view` tool to read provider-only images, set `Capabilities.Binary` and return a provider implementing `SessionFSBinaryProvider` (`ReadFileBytes` and `WriteFileBytes`) from `CreateSessionFSProvider`. Image reads do not fall back to local files. Binary reads and writes are limited to 50,330,880 bytes before encoding or decoding. **SessionConfig:** @@ -367,6 +418,7 @@ Initial acquisition runs during session creation or resume. Cancellation, provid - `Send(ctx context.Context, options MessageOptions) (string, error)` - Send a message - `On(handler SessionEventHandler) func()` - Subscribe to events (returns unsubscribe function) - `Abort(ctx context.Context) error` - Abort the currently processing message +- `SetTools(ctx context.Context, tools []Tool) error` - Replace this client's live custom tools and handlers for the session. See [changing tools](../docs/features/changing-tools.md) for shared behavior and active-turn limitations. - `GetEvents(ctx context.Context) ([]SessionEvent, error)` - Get event history - `Disconnect() error` - Disconnect the session (releases in-memory resources, preserves disk state) - `UI() *SessionUI` - Interactive UI API for elicitation dialogs @@ -445,6 +497,8 @@ Available section constants: `SectionPreamble`, `SectionIdentity`, `SectionTone` `SectionIdentity` and `SectionToolInstructions` are section _groups_ that target a collection of related sub-sections as a unit. Use `SectionPreamble` to target just the identity preamble without affecting its sibling sub-sections. +`SectionLastInstructions` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a `SectionOverride.Transform` callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SectionRuntimeInstructions` is a separate section: removing it does not remove `SectionLastInstructions`. + Each section override supports five actions: - **`replace`** — Replace the section content entirely @@ -601,6 +655,26 @@ editFile := copilot.DefineTool("edit_file", "Custom file editor with project-spe editFile.OverridesBuiltInTool = true ``` +An explicit `apply_patch` override can declare a root string schema by using a +`string` parameter with `DefineTool`. The model sees a required `input` property, +but the runtime restores the scalar patch text before dispatch. Both the typed +parameter and `ToolInvocation.Arguments` receive a string, not an +`{"input": ...}` object. This example returns trimmed patch text; replace the +handler body with your own patch implementation: + +```go +import "strings" + +applyPatch := copilot.DefineTool("apply_patch", "Apply a patch", + func(patch string, inv copilot.ToolInvocation) (string, error) { + return strings.TrimSpace(patch), nil + }) +applyPatch.OverridesBuiltInTool = true +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `SkipPermission = true` on a tool to allow it to execute without triggering a permission prompt: @@ -955,6 +1029,17 @@ session, err := client.ResumeSession(context.Background(), sessionID, &copilot.R }) ``` +`AllowTranscriptRecovery` controls transcript repair on resume. A nil value omits +the wire field and uses the runtime default (true) in all modes. +Set `copilot.Bool(false)` to reject recovery. +`session.TranscriptRecovery()` returns repair details or nil. The report's +`InvalidLineNumbers` includes any discarded torn-tail lines. On rejection, +use `errors.As(err, &rpcErr)` with `var rpcErr *copilot.RPCError` to inspect +`rpcErr.Code` and `rpcErr.Data` (`invalidLineNumbers`, `sessionStartMoved`); +the original error message remains available. +Disabling recovery still permits adding a missing newline after an intact final +record; it rejects torn tails. + ### Per-Tool Skip Permission To let a specific custom tool bypass the permission prompt entirely, set `SkipPermission = true` on the tool. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. @@ -1045,6 +1130,22 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi return nil, nil }, + // Called before a subagent's first turn; context is prepended to its prompt + OnSubagentStart: func(input copilot.SubagentStartHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStartHookOutput, error) { + fmt.Printf("Starting subagent %s\n", input.AgentName) + return &copilot.SubagentStartHookOutput{ + AdditionalContext: "Focus on the requested file.", + }, nil + }, + + // Called after a subagent finishes a turn + OnSubagentStop: func(input copilot.SubagentStopHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStopHookOutput, error) { + fmt.Printf("Subagent %s finished: %s\n", input.AgentType, input.StopReason) + return &copilot.SubagentStopHookOutput{ + ModifiedResponse: copilot.String("Verified: " + input.Response), + }, nil + }, + // Called when an error occurs OnErrorOccurred: func(input copilot.ErrorOccurredHookInput, invocation copilot.HookInvocation) (*copilot.ErrorOccurredHookOutput, error) { fmt.Printf("Error in %s: %s\n", input.ErrorContext, input.Error) @@ -1064,8 +1165,13 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi - `OnUserPromptSubmitted` - Intercept user prompts. Can modify the prompt before processing. - `OnSessionStart` - Run logic when a session starts or resumes. - `OnSessionEnd` - Cleanup or logging when session ends. +- `OnSubagentStart` - Observe a subagent before its first turn and optionally prepend `AdditionalContext` to its prompt. +- `OnSubagentStop` - Observe a completed subagent turn. Return `Decision: "block"` with a non-empty `Reason` to request another turn, **or** `ModifiedResponse: copilot.String("replacement")` to replace its response (including an intentional empty string). +- `OnAgentStop` - Intercept the parent agent's stop; distinct from `OnSubagentStop`. - `OnErrorOccurred` - Handle errors with retry/skip/abort strategies. +Subagent hook inputs identify the **parent** session in `SessionID` and `HookInvocation.SessionID`. Both include a `time.Time` timestamp, `WorkingDirectory` (`cwd`), `TranscriptPath`, and `AgentName`. `AgentDisplayName` and `AgentDescription` may be empty. Stop inputs additionally include an optional `AgentID`, `AgentType`, `StopReason` (`"end_turn"`), and the subagent's `Response`. Registering either hook alone enables hook callbacks for both new and resumed sessions. + ## Commands Register slash-commands that users can invoke from the CLI TUI. When a user types `/deploy production`, the SDK dispatches to your handler and responds via the RPC layer. diff --git a/go/canvas.go b/go/canvas.go index e31598bb1e..0de02e4c38 100644 --- a/go/canvas.go +++ b/go/canvas.go @@ -112,6 +112,11 @@ func CanvasErrorNoHandler() *CanvasError { type CanvasHandler interface { OnOpen(ctx context.Context, c rpc.CanvasProviderOpenRequest) (rpc.CanvasProviderOpenResult, error) OnClose(ctx context.Context, c rpc.CanvasProviderCloseRequest) error + // OnAction handles a non-lifecycle action declared by the canvas. The + // returned value is sent to the model as the invoke_canvas_action tool + // result. To return text and images, as tool handlers do, return a + // ToolResult with BinaryResultsForLLM; any other value is rendered to the + // model as JSON text. OnAction(ctx context.Context, c rpc.CanvasProviderInvokeActionRequest) (any, error) } diff --git a/go/client.go b/go/client.go index 05d2104ee5..1a761612b6 100644 --- a/go/client.go +++ b/go/client.go @@ -42,6 +42,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime/debug" "strconv" "strings" "sync" @@ -60,6 +61,12 @@ import ( // whole-session [ProviderConfig]. Named providers are keyed by their own Name. const defaultBearerTokenProviderName = "default" +type cloudSkillProviderError struct{} + +func (cloudSkillProviderError) Error() string { + return "Skill providers are not supported for cloud sessions." +} + // collectBearerTokenProviders gathers the per-provider [BearerTokenProvider] callbacks // from the singular provider and any named providers, keyed by provider name. The // singular provider uses the implicit name "default"; named providers use their @@ -543,8 +550,10 @@ func (c *Client) Start(ctx context.Context) error { } if c.options.SessionFS.Capabilities != nil { sqlite := c.options.SessionFS.Capabilities.Sqlite + binary := c.options.SessionFS.Capabilities.Binary req.Capabilities = &rpc.SessionFSSetProviderCapabilities{ Sqlite: &sqlite, + Binary: &binary, } } _, err := c.RPC.SessionFS.SetProvider(ctx, req) @@ -745,6 +754,7 @@ func (c *Client) ForceStop() { c.sessions = make(map[string]*Session) c.sessionsMux.Unlock() for _, session := range sessions { + session.clearSkillProvider() session.cancelPendingExternalTools() } c.clearGitHubTokenProviders() @@ -874,6 +884,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config == nil { config = &SessionConfig{} } + if config.Cloud != nil && config.SkillProvider != nil { + return nil, cloudSkillProviderError{} + } if config.GitHubToken != "" && config.GitHubTokenProvider != nil { return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") } @@ -911,6 +924,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.EnableHostGitOperations = config.EnableHostGitOperations req.EnableSessionStore = config.EnableSessionStore req.EnableSkills = config.EnableSkills + if config.SkillProvider != nil { + req.HasSkillProvider = Bool(true) + } req.Tools = config.Tools systemMessage := c.systemMessageForMode(config.SystemMessage) wireSystemMessage, transformCallbacks := extractTransformCallbacks(systemMessage) @@ -1022,7 +1038,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses config.Hooks.OnSessionStart != nil || config.Hooks.OnSessionEnd != nil || config.Hooks.OnErrorOccurred != nil || - config.Hooks.OnAgentStop != nil) { + config.Hooks.OnAgentStop != nil || + config.Hooks.OnSubagentStart != nil || + config.Hooks.OnSubagentStop != nil) { req.Hooks = Bool(true) } if config.OnPermissionRequest != nil { @@ -1108,6 +1126,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.CanvasHandler != nil { s.registerCanvasHandler(config.CanvasHandler) } + if config.SkillProvider != nil { + s.registerSkillProvider(config.SkillProvider) + } if bearerTokenProviders := collectBearerTokenProviders(config.Provider, config.Providers); bearerTokenProviders != nil { s.registerBearerTokenProviders(bearerTokenProviders) } @@ -1124,6 +1145,12 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return nil, fmt.Errorf("SessionFS capabilities declare SQLite support but the provider does not implement SessionFSSqliteProvider") } } + if c.options.SessionFS.Capabilities != nil && c.options.SessionFS.Capabilities.Binary { + if _, ok := provider.(SessionFSBinaryProvider); !ok { + unregisterSession(sessionID, s) + return nil, fmt.Errorf("SessionFS capabilities declare binary support but the provider does not implement SessionFSBinaryProvider") + } + } s.clientSessionAPIs.SessionFS = newSessionFSAdapter(provider) } @@ -1135,6 +1162,14 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses var session *Session var registeredSessionID string + var orphanedSessionID string + var inlineMu sync.Mutex + var requestFinished bool + deleteOrphan := func(sessionID string) error { + cleanupCtx, cancelCleanup := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) + defer cancelCleanup() + return c.DeleteSession(cleanupCtx, sessionID) + } // Pre-register non-cloud sessions BEFORE issuing the RPC so any // session-scoped requests the CLI emits during session.create processing @@ -1169,19 +1204,48 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return fmt.Errorf("session.create response did not include a sessionId") } s, err := initializeSession(early.SessionID) + inlineMu.Lock() if err != nil { + if requestFinished { + // Cleanup must not wait for an RPC response on the read loop. + go func() { + if cleanupErr := deleteOrphan(early.SessionID); cleanupErr != nil { + log.Printf("failed to delete uninitialized cloud session %s: %v", early.SessionID, cleanupErr) + } + }() + } else { + orphanedSessionID = early.SessionID + } + inlineMu.Unlock() return err } + if requestFinished { + inlineMu.Unlock() + unregisterSession(early.SessionID, s) + return nil + } session = s registeredSessionID = early.SessionID + inlineMu.Unlock() return nil } } result, err := c.client.RequestWithInlineResponse(ctx, "session.create", req, inlineCb) + inlineMu.Lock() + requestFinished = true + cleanupSessionID := orphanedSessionID + sessionToUnregister := session + registeredID := registeredSessionID + inlineMu.Unlock() if err != nil { - if registeredSessionID != "" { - unregisterSession(registeredSessionID, session) + if registeredID != "" { + unregisterSession(registeredID, sessionToUnregister) + } + if cleanupSessionID != "" { + if cleanupErr := deleteOrphan(cleanupSessionID); cleanupErr != nil { + err = errors.Join(err, fmt.Errorf("failed to delete uninitialized cloud session: %w", cleanupErr)) + } } return nil, fmt.Errorf("failed to create session: %w", err) } @@ -1347,7 +1411,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, config.Hooks.OnSessionStart != nil || config.Hooks.OnSessionEnd != nil || config.Hooks.OnErrorOccurred != nil || - config.Hooks.OnAgentStop != nil) { + config.Hooks.OnAgentStop != nil || + config.Hooks.OnSubagentStart != nil || + config.Hooks.OnSubagentStop != nil) { req.Hooks = Bool(true) } req.WorkingDirectory = config.WorkingDirectory @@ -1362,10 +1428,14 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.EnableHostGitOperations = config.EnableHostGitOperations req.EnableSessionStore = config.EnableSessionStore req.EnableSkills = config.EnableSkills + if config.SkillProvider != nil { + req.HasSkillProvider = Bool(true) + } if config.SuppressResumeEvent { req.DisableResume = Bool(true) } req.ContinuePendingWork = config.ContinuePendingWork + req.AllowTranscriptRecovery = config.AllowTranscriptRecovery req.MCPServers = config.MCPServers req.Diagnostics = config.Diagnostics req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage @@ -1470,6 +1540,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.CanvasHandler != nil { session.registerCanvasHandler(config.CanvasHandler) } + if config.SkillProvider != nil { + session.registerSkillProvider(config.SkillProvider) + } if bearerTokenProviders := collectBearerTokenProviders(config.Provider, config.Providers); bearerTokenProviders != nil { session.registerBearerTokenProviders(bearerTokenProviders) } @@ -1486,6 +1559,12 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, return nil, fmt.Errorf("SessionFS capabilities declare SQLite support but the provider does not implement SessionFSSqliteProvider") } } + if c.options.SessionFS.Capabilities != nil && c.options.SessionFS.Capabilities.Binary { + if _, ok := provider.(SessionFSBinaryProvider); !ok { + session.stopEventProcessing() + return nil, fmt.Errorf("SessionFS capabilities declare binary support but the provider does not implement SessionFSBinaryProvider") + } + } session.clientSessionAPIs.SessionFS = newSessionFSAdapter(provider) } @@ -1537,6 +1616,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } session.workspacePath = response.WorkspacePath + session.transcriptRecovery = response.TranscriptRecovery session.setCapabilities(response.Capabilities) session.setOpenCanvases(response.OpenCanvases) @@ -1686,6 +1766,7 @@ func (c *Client) DeleteSession(ctx context.Context, sessionID string) error { delete(c.sessions, sessionID) c.sessionsMux.Unlock() if session != nil { + session.clearSkillProvider() session.releaseGitHubTokenProviderRegistration() } @@ -2533,6 +2614,8 @@ func (c *Client) setupNotificationHandler() { c.client.SetRequestHandler("exitPlanMode.request", jsonrpc2.RequestHandlerFor(c.handleExitPlanModeRequest)) c.client.SetRequestHandler("autoModeSwitch.request", jsonrpc2.RequestHandlerFor(c.handleAutoModeSwitchRequest)) c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform)) + c.client.SetRequestContextHandler("skillProvider.list", c.handleSkillProviderList) + c.client.SetRequestContextHandler("skillProvider.read", c.handleSkillProviderRead) rpc.RegisterClientSessionAPIHandlers(c.client, func(sessionID string) *rpc.ClientSessionAPIHandlers { c.sessionsMux.Lock() defer c.sessionsMux.Unlock() @@ -2627,6 +2710,7 @@ func (c *Client) handleConnectionClose() { } c.sessionsMux.Unlock() for _, session := range sessions { + session.clearSkillProvider() session.cancelPendingExternalTools() } // Avoid deadlocking with Stop/ForceStop, which hold startStopMux while @@ -2744,6 +2828,102 @@ func (c *Client) handleSessionEvent(req sessionEventRequest) { } } +func (c *Client) handleSkillProviderList(ctx context.Context, params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var req rpc.SkillProviderListRequest + if err := json.Unmarshal(params, &req); err != nil || req.SessionID == "" { + return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid skill provider list payload"} + } + + provider, rpcErr := c.resolveSkillProvider(req.SessionID) + if rpcErr != nil { + return nil, rpcErr + } + + skills, err := callSkillProvider(ctx, req.SessionID, "listSkills", func() ([]rpc.SkillProviderDescriptor, error) { + return provider.ListSkills(ctx) + }) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "Skill provider listSkills failed"} + } + if skills == nil { + skills = []rpc.SkillProviderDescriptor{} + } + + raw, err := json.Marshal(rpc.SkillProviderListResult{Skills: skills}) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil +} + +func (c *Client) handleSkillProviderRead(ctx context.Context, params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var req rpc.SkillProviderReadRequest + if err := json.Unmarshal(params, &req); err != nil || req.SessionID == "" || req.Name == "" { + return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid skill provider read payload"} + } + + provider, rpcErr := c.resolveSkillProvider(req.SessionID) + if rpcErr != nil { + return nil, rpcErr + } + + // Classify not-found inside the guarded call: errors.Is runs provider + // Is/Unwrap methods, which may panic. + markdown, err := callSkillProvider(ctx, req.SessionID, "readSkill", func() (*string, error) { + markdown, err := provider.ReadSkill(ctx, req.Name) + if errors.Is(err, ErrSkillNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + return &markdown, nil + }) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: "Skill provider readSkill failed"} + } + + raw, err := json.Marshal(rpc.SkillProviderReadResult{Markdown: markdown}) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil +} + +func (c *Client) resolveSkillProvider(sessionID string) (SkillProvider, *jsonrpc2.Error) { + c.sessionsMux.Lock() + session := c.sessions[sessionID] + c.sessionsMux.Unlock() + if session == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No skill provider for session: %s", sessionID)} + } + provider := session.getSkillProvider() + if provider == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No skill provider for session: %s", sessionID)} + } + return provider, nil +} + +var errSkillProviderPanic = errors.New("skill provider panicked") + +// callSkillProvider logs provider failures locally and converts panics into +// an error, so the runtime only ever receives a generic failure. +func callSkillProvider[T any](ctx context.Context, sessionID, operation string, call func() (T, error)) (result T, err error) { + defer func() { + if failure := recover(); failure != nil { + log.Printf("skill provider %s panicked: session_id=%s panic=%v\n%s", operation, sessionID, failure, debug.Stack()) + var zero T + result, err = zero, errSkillProviderPanic + } + }() + result, err = call() + // A call the runtime cancelled is expected to fail; it isn't a provider failure. + if err != nil && ctx.Err() == nil { + log.Printf("skill provider %s failed: session_id=%s error=%v", operation, sessionID, err) + } + return result, err +} + // handleUserInputRequest handles a user input request from the CLI server. func (c *Client) handleUserInputRequest(req userInputRequest) (*userInputResponse, *jsonrpc2.Error) { if req.SessionID == "" || req.Question == "" { diff --git a/go/client_test.go b/go/client_test.go index 8646ab682a..0f701a3359 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1,8 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + package copilot import ( + "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -805,6 +809,76 @@ func TestClient_ForwardsAskUserVariantToSessionRequests(t *testing.T) { assertAskUserVariant(t, <-resumeParams, "") } +func TestClient_EnablesSubagentHooksForCreateAndResume(t *testing.T) { + for _, tt := range []struct { + name string + hooks *SessionHooks + }{ + { + name: "start only", + hooks: &SessionHooks{ + OnSubagentStart: func(SubagentStartHookInput, HookInvocation) (*SubagentStartHookOutput, error) { + return nil, nil + }, + }, + }, + { + name: "stop only", + hooks: &SessionHooks{ + OnSubagentStop: func(SubagentStopHookInput, HookInvocation) (*SubagentStopHookOutput, error) { + return nil, nil + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + requests := make(chan json.RawMessage, 2) + for _, method := range []string{"session.create", "session.resume"} { + server.SetRequestHandler(method, func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + requests <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + } + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + created, err := client.CreateSession(t.Context(), &SessionConfig{Hooks: tt.hooks}) + if err != nil { + t.Fatalf("create session: %v", err) + } + if err := created.Disconnect(); err != nil { + t.Fatalf("disconnect session: %v", err) + } + resumed, err := client.ResumeSession(t.Context(), created.SessionID, &ResumeSessionConfig{Hooks: tt.hooks}) + if err != nil { + t.Fatalf("resume session: %v", err) + } + if err := resumed.Disconnect(); err != nil { + t.Fatalf("disconnect resumed session: %v", err) + } + + for _, method := range []string{"session.create", "session.resume"} { + var request map[string]any + if err := json.Unmarshal(<-requests, &request); err != nil { + t.Fatalf("decode %s request: %v", method, err) + } + if request["hooks"] != true { + t.Errorf("%s hooks = %v, want true", method, request["hooks"]) + } + } + }) + } +} + func TestClient_ForwardsDiagnosticsToSessionRequests(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) @@ -1447,6 +1521,63 @@ func (noSQLiteSessionFSProvider) ReadDirectoryWithTypes(string) ([]rpc.SessionFS func (noSQLiteSessionFSProvider) Remove(string, bool, bool) error { return nil } func (noSQLiteSessionFSProvider) Rename(string, string) error { return nil } +type oversizedBinarySessionFSProvider struct{ noSQLiteSessionFSProvider } + +func (oversizedBinarySessionFSProvider) ReadFileBytes(string) ([]byte, error) { + return make([]byte, maxSessionFSBinaryBytes+1), nil +} + +func (oversizedBinarySessionFSProvider) WriteFileBytes(string, []byte, *int) error { return nil } + +type recordingBinarySessionFSProvider struct { + noSQLiteSessionFSProvider + content []byte + mode *int +} + +func (p *recordingBinarySessionFSProvider) ReadFileBytes(string) ([]byte, error) { + return p.content, nil +} +func (p *recordingBinarySessionFSProvider) WriteFileBytes(_ string, content []byte, mode *int) error { + p.content, p.mode = content, mode + return nil +} + +func TestSessionFSAdapterWritesExactBytesAndRejectsInvalidInput(t *testing.T) { + provider := &recordingBinarySessionFSProvider{} + adapter := newSessionFSAdapter(provider) + mode := int64(0o600) + write := &rpc.SessionFSWriteFileBytesRequest{ + SessionID: "session", Path: "/image.png", Content: "AP/+AQ==", Mode: &mode, + } + result, err := adapter.WriteFileBytes(write) + if err != nil || result != nil || !bytes.Equal(provider.content, []byte{0, 255, 254, 1}) || + provider.mode == nil || *provider.mode != 0o600 { + t.Fatalf("binary write lost content/mode: result=%v err=%v bytes=%v mode=%v", result, err, provider.content, provider.mode) + } + for _, content := range []string{"AA==AAAA", "AA==\r\n", strings.Repeat("A", maxSessionFSBinaryContentLength+1)} { + result, err := adapter.WriteFileBytes(&rpc.SessionFSWriteFileBytesRequest{ + SessionID: "session", Path: "/image.png", Content: content, + }) + if err != nil || result == nil || !bytes.Equal(provider.content, []byte{0, 255, 254, 1}) { + t.Fatalf("invalid binary write modified file: result=%v err=%v bytes=%v", result, err, provider.content) + } + } +} + +func TestSessionFSAdapterRejectsOversizedBinaryRead(t *testing.T) { + result, err := newSessionFSAdapter(oversizedBinarySessionFSProvider{}).ReadFileBytes( + &rpc.SessionFSReadFileBytesRequest{SessionID: "session", Path: "/image.png"}, + ) + if err != nil { + t.Fatalf("ReadFileBytes returned a transport error: %v", err) + } + if result.Content != "" || result.Error == nil || result.Error.Code != rpc.SessionFSErrorCodeUNKNOWN || + result.Error.Message == nil || !strings.Contains(*result.Error.Message, "binary read limit") { + t.Fatalf("expected a filesystem error without encoded bytes, got %+v", result) + } +} + func assertRuntimeShutdownNotCalled(t *testing.T, shutdownCalled <-chan struct{}) { t.Helper() select { @@ -1498,6 +1629,113 @@ func TestClient_SessionFSConfig(t *testing.T) { }) } +func TestClient_CreateSessionDeletesCloudSessionWhenBinaryProviderValidationFails(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Binary: true}, + }, + }) + defer cleanup() + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Cloud: &CloudSessionOptions{ + Repository: &CloudSessionRepository{Owner: "github", Name: "copilot-sdk", Branch: "main"}, + }, + OnPermissionRequest: PermissionHandler.ApproveAll, + CreateSessionFSProvider: func(*Session) SessionFSProvider { + return nil + }, + }) + if err == nil || !strings.Contains(err.Error(), "does not implement SessionFSBinaryProvider") { + t.Fatalf("expected binary provider validation error, got %v", err) + } + snapshot := requests.snapshot() + if len(snapshot) != 2 || snapshot[0].Method != "session.create" || snapshot[1].Method != "session.delete" { + t.Fatalf("expected create then orphan cleanup, got %+v", snapshot) + } + if snapshot[1].Params["sessionId"] != "server-assigned-session-1" { + t.Fatalf("expected cleanup of server-assigned session, got %+v", snapshot[1]) + } + client.sessionsMux.Lock() + defer client.sessionsMux.Unlock() + if len(client.sessions) != 0 { + t.Fatalf("failed creation left %d active sessions", len(client.sessions)) + } +} + +func TestClient_CreateSessionDeletesCloudSessionWhenProviderValidationFailsAfterCancellation(t *testing.T) { + client, requests, cleanup := newInMemoryClientWithOptions(t, &ClientOptions{ + SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Binary: true}, + }, + }) + defer cleanup() + requests.notify = make(chan struct{}, 1) + + providerStarted := make(chan struct{}) + releaseProvider := make(chan struct{}) + release := sync.OnceFunc(func() { close(releaseProvider) }) + defer release() + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + createDone := make(chan error, 1) + go func() { + _, err := client.CreateSession(ctx, &SessionConfig{ + Cloud: &CloudSessionOptions{Repository: &CloudSessionRepository{Owner: "github", Name: "copilot-sdk", Branch: "main"}}, + OnPermissionRequest: PermissionHandler.ApproveAll, + CreateSessionFSProvider: func(*Session) SessionFSProvider { + close(providerStarted) + <-releaseProvider + return noSQLiteSessionFSProvider{} + }, + }) + createDone <- err + }() + + deadline := time.NewTimer(5 * time.Second) + defer deadline.Stop() + select { + case <-providerStarted: + case <-deadline.C: + t.Fatal("session.create did not reach the provider factory") + } + cancel() + select { + case err := <-createDone: + if !errors.Is(err, context.Canceled) { + t.Fatalf("CreateSession error = %v, want context cancellation", err) + } + case <-deadline.C: + t.Fatal("CreateSession did not return after cancellation") + } + release() + + for { + snapshot := requests.snapshot() + if len(snapshot) >= 2 { + if len(snapshot) != 2 || snapshot[0].Method != "session.create" || snapshot[1].Method != "session.delete" { + t.Fatalf("expected one create and one orphan cleanup, got %+v", snapshot) + } + if snapshot[1].Params["sessionId"] != "server-assigned-session-1" { + t.Fatalf("expected cleanup of server-assigned session, got %+v", snapshot[1]) + } + return + } + select { + case <-requests.notify: + case <-deadline.C: + t.Fatalf("cloud session was not deleted after provider validation failed; requests: %+v", snapshot) + } + } +} + func TestClient_AuthOptions(t *testing.T) { t.Run("should accept GitHubToken option", func(t *testing.T) { client := NewClient(&ClientOptions{ @@ -3136,12 +3374,19 @@ type recordedRequest struct { type requestRecorder struct { mu sync.Mutex requests []recordedRequest + notify chan struct{} } func (r *requestRecorder) append(request recordedRequest) { r.mu.Lock() defer r.mu.Unlock() r.requests = append(r.requests, request) + if r.notify != nil { + select { + case r.notify <- struct{}{}: + default: + } + } } func (r *requestRecorder) snapshot() []recordedRequest { @@ -3230,6 +3475,8 @@ func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeW result = map[string]any{} case "session.detach": result = map[string]any{"success": true} + case "session.delete": + result = map[string]any{"success": true} default: t.Errorf("unexpected JSON-RPC method %s", request.Method) return @@ -3892,6 +4139,108 @@ func TestModeCallbackRequestHandlers(t *testing.T) { } } +func TestResumeTranscriptRecoveryDefaultsAndReport(t *testing.T) { + for _, tc := range []struct { + name string + mode ClientMode + explicit *bool + want *bool + }{ + {"CLI default", ModeCopilotCli, nil, nil}, + {"empty default", ModeEmpty, nil, nil}, + {"CLI strict", ModeCopilotCli, Bool(false), Bool(false)}, + {"CLI recovery", ModeCopilotCli, Bool(true), Bool(true)}, + {"empty strict", ModeEmpty, Bool(false), Bool(false)}, + {"empty recovery", ModeEmpty, Bool(true), Bool(true)}, + } { + t.Run(tc.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + options: ClientOptions{Mode: tc.mode}, + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + resumeParams := make(chan json.RawMessage, 2) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + if sessionIDFromParams(t, params) == "unrepaired" { + return []byte(`{"sessionId":"unrepaired"}`), nil + } + return []byte(`{"sessionId":"s1","transcriptRecovery":{"plannedBackupPath":"backup.jsonl","invalidLineNumbers":[3,4],"sessionStartMoved":true}}`), nil + }) + server.SetRequestHandler("session.options.update", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + session, err := client.ResumeSessionWithOptions(t.Context(), "s1", &ResumeSessionConfig{ + AllowTranscriptRecovery: tc.explicit, + AvailableTools: []string{}, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + t.Cleanup(func() { + if err := session.Disconnect(); err != nil { + t.Errorf("session disconnect failed: %v", err) + } + }) + var wire map[string]any + if err := json.Unmarshal(<-resumeParams, &wire); err != nil { + t.Fatal(err) + } + value, present := wire["allowTranscriptRecovery"] + if tc.want == nil { + if present { + t.Fatalf("unexpected allowTranscriptRecovery: %v", value) + } + } else if !present || value != *tc.want { + t.Fatalf("allowTranscriptRecovery = %v (present %v), want %v", value, present, *tc.want) + } + report := session.TranscriptRecovery() + if report == nil || report.PlannedBackupPath != "backup.jsonl" || !reflect.DeepEqual(report.InvalidLineNumbers, []int{3, 4}) || !report.SessionStartMoved { + t.Fatalf("unexpected returned transcript recovery report: %+v", report) + } + unrepaired, err := client.ResumeSessionWithOptions(t.Context(), "unrepaired", &ResumeSessionConfig{ + AvailableTools: []string{}, + }) + if err != nil { + t.Fatalf("unrepaired ResumeSessionWithOptions failed: %v", err) + } + t.Cleanup(func() { + if err := unrepaired.Disconnect(); err != nil { + t.Errorf("unrepaired session disconnect failed: %v", err) + } + }) + if unrepaired.TranscriptRecovery() != nil { + t.Fatalf("unexpected recovery report on unrepaired session: %+v", unrepaired.TranscriptRecovery()) + } + }) + } +} + +func TestTranscriptRecoveryReportCopyIsolation(t *testing.T) { + session := &Session{transcriptRecovery: &TranscriptRecoveryReport{ + PlannedBackupPath: "backup.jsonl", + InvalidLineNumbers: []int{3, 4}, + SessionStartMoved: true, + }} + report := session.TranscriptRecovery() + if report == nil || report.PlannedBackupPath != "backup.jsonl" || !reflect.DeepEqual(report.InvalidLineNumbers, []int{3, 4}) || !report.SessionStartMoved { + t.Fatalf("unexpected transcript recovery report: %+v", report) + } + report.InvalidLineNumbers[0] = 99 + if session.TranscriptRecovery().InvalidLineNumbers[0] != 3 { + t.Fatal("report should not expose the session's slice") + } + if (&Session{}).TranscriptRecovery() != nil { + t.Fatal("new session should have no recovery report") + } +} + func TestResumeSessionRequest_ContinuePendingWork(t *testing.T) { t.Run("forwards continuePendingWork when true", func(t *testing.T) { req := resumeSessionRequest{ @@ -4842,6 +5191,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { Deny: []string{"Shell(git push)"}, Ask: []string{"Domain(publish.example)"}, Allow: []string{"Read(**)"}, + LimitTo: []string{"Domain(github.com)"}, }, } @@ -4850,6 +5200,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { "deny": []any{"Shell(git push)"}, "ask": []any{"Domain(publish.example)"}, "allow": []any{"Read(**)"}, + "limitTo": []any{"Domain(github.com)"}, } t.Run("direct injection enables managed safeguards", func(t *testing.T) { @@ -4941,6 +5292,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { Deny: []string{}, Ask: []string{}, Allow: []string{}, + LimitTo: []string{}, }, }} data, err := json.Marshal(req) @@ -4953,7 +5305,7 @@ func TestSessionRequests_ManagedSettings(t *testing.T) { if perms["disableBypassPermissionsMode"] != "disable" { t.Errorf("Expected disableBypassPermissionsMode preserved, got %v", perms["disableBypassPermissionsMode"]) } - for _, key := range []string{"deny", "ask", "allow"} { + for _, key := range []string{"deny", "ask", "allow", "limitTo"} { if value, ok := perms[key].([]any); !ok || len(value) != 0 { t.Errorf("Expected %s to be an explicit empty array, got %v", key, perms[key]) } diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index 41b5863347..f7a34413d1 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -41,6 +41,7 @@ const ( packageJSONURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package.json" packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" defaultCLIDownloadBaseURL = "https://github.com/github/copilot-cli/releases/download" + unstableDownloadBaseURL = "https://github.com/github/copilot-sdk/releases/download" cliDownloadBaseURLEnvironment = "COPILOT_CLI_DOWNLOAD_BASE_URL" defaultPackageName = "main" ) @@ -540,7 +541,7 @@ func runtimeWrapperName(binaryName string) string { } var hostlessExcludedTopLevel = map[string]bool{ - "app.js": true, "assets": true, "changelog.json": true, "copilot": true, "copilot.exe": true, + "app.js": true, "assets": true, "changelog.json": true, "cli-main.js": true, "copilot": true, "copilot.exe": true, "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, "LICENSE.md": true, "npm-loader.js": true, "package.json": true, "pvrecorder": true, "queries": true, "README.md": true, "sea-loader.js": true, "webview": true, @@ -925,14 +926,18 @@ func mustDecodeBase64(s string) []byte { } var ( - releaseChecksumCache = map[string]map[string]string{} - releaseHTTPClient = &http.Client{Timeout: 10 * time.Minute} + releaseChecksumCache = map[string]map[string]string{} + releaseHTTPClient = &http.Client{Timeout: 10 * time.Minute} + unstableRuntimeVersion = regexp.MustCompile(`^(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*)){2}(?:-unstable|-(?:0|[1-9][0-9]*)\.unstable)\.r[1-9][0-9]*\.g[0-9a-f]{7}$`) ) -func cliDownloadBaseURL() string { +func cliDownloadBaseURL(version string) string { if override := strings.TrimRight(os.Getenv(cliDownloadBaseURLEnvironment), "/"); override != "" { return override } + if unstableRuntimeVersion.MatchString(version) { + return unstableDownloadBaseURL + } return defaultCLIDownloadBaseURL } @@ -941,7 +946,11 @@ func releaseAssetName(version, runtimePlatform string) string { } func releaseDownloadURL(version, assetName string) string { - return fmt.Sprintf("%s/v%s/%s", cliDownloadBaseURL(), version, assetName) + tag := "v" + version + if unstableRuntimeVersion.MatchString(version) { + tag = "runtime-" + version + } + return fmt.Sprintf("%s/%s/%s", cliDownloadBaseURL(version), tag, assetName) } func parseReleaseChecksums(contents string) map[string]string { @@ -958,11 +967,10 @@ func parseReleaseChecksums(contents string) map[string]string { } func getReleaseChecksum(version, assetName string) (string, error) { - baseURL := cliDownloadBaseURL() - cacheKey := baseURL + "\x00" + version + checksumsURL := releaseDownloadURL(version, "SHA256SUMS.txt") + cacheKey := checksumsURL checksums, ok := releaseChecksumCache[cacheKey] if !ok { - checksumsURL := fmt.Sprintf("%s/v%s/SHA256SUMS.txt", baseURL, version) fmt.Printf("Downloading checksums from %s...\n", checksumsURL) resp, err := releaseHTTPClient.Get(checksumsURL) if err != nil { diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index 7dd376d40b..90bdea1689 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -30,6 +30,7 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t "package/preloads/extension_bootstrap.mjs": "preload", "package/sdk/factory.js": "factory", "package/app.js": "excluded", + "package/cli-main.js": "excluded", "package/LICENSE.md": "excluded", "package/README.md": "excluded", }) @@ -50,7 +51,7 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t t.Fatalf("retained assets = %#v", files) } for _, excluded := range []string{ - "runtime.node", "copilot-runtime", "app.js", "LICENSE.md", "README.md", + "runtime.node", "copilot-runtime", "app.js", "cli-main.js", "LICENSE.md", "README.md", } { if _, ok := files[excluded]; ok { t.Fatalf("excluded asset %q was retained", excluded) @@ -59,46 +60,82 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t } func TestDownloadCLIBinaryUsesVerifiedReleasePackage(t *testing.T) { - dir := t.TempDir() - archivePath := filepath.Join(dir, "source.tgz") - writeTarGz(t, archivePath, map[string]string{ - "package/prebuilds/linux-x64/copilot-runtime": "runtime wrapper", - }) - archive, err := os.ReadFile(archivePath) - if err != nil { - t.Fatal(err) - } - checksum := fmt.Sprintf("%x", sha256.Sum256(archive)) - version := "1.2.3" - assetName := releaseAssetName(version, "linux-x64") - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - switch request.URL.Path { - case "/v1.2.3/SHA256SUMS.txt": - fmt.Fprintf(writer, "%s %s\n", checksum, assetName) - case "/v1.2.3/" + assetName: - writer.Write(archive) - default: - http.NotFound(writer, request) - } - })) - defer server.Close() - t.Setenv(cliDownloadBaseURLEnvironment, server.URL) - releaseChecksumCache = map[string]map[string]string{} + for _, version := range []string{"1.2.3", "1.2.3-4.unstable.r123.gabcdef0"} { + t.Run(version, func(t *testing.T) { + dir := t.TempDir() + archivePath := filepath.Join(dir, "source.tgz") + writeTarGz(t, archivePath, map[string]string{ + "package/prebuilds/linux-x64/copilot-runtime": "runtime wrapper", + }) + archive, err := os.ReadFile(archivePath) + if err != nil { + t.Fatal(err) + } + checksum := fmt.Sprintf("%x", sha256.Sum256(archive)) + tag := "v" + version + if strings.Contains(version, ".unstable.") { + tag = "runtime-" + version + } + assetName := releaseAssetName(version, "linux-x64") + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/" + tag + "/SHA256SUMS.txt": + fmt.Fprintf(writer, "%s %s\n", checksum, assetName) + case "/" + tag + "/" + assetName: + writer.Write(archive) + default: + http.NotFound(writer, request) + } + })) + defer server.Close() + t.Setenv(cliDownloadBaseURLEnvironment, server.URL) + releaseChecksumCache = map[string]map[string]string{} - binaryPath, downloadedArchive, err := downloadCLIBinary( - "linux-x64", - "copilot", - version, - t.TempDir(), - ) - if err != nil { - t.Fatal(err) - } - if got, err := os.ReadFile(binaryPath); err != nil || string(got) != "runtime wrapper" { - t.Fatalf("downloaded CLI = %q, %v", got, err) + binaryPath, downloadedArchive, err := downloadCLIBinary( + "linux-x64", + "copilot", + version, + t.TempDir(), + ) + if err != nil { + t.Fatal(err) + } + if got, err := os.ReadFile(binaryPath); err != nil || string(got) != "runtime wrapper" { + t.Fatalf("downloaded CLI = %q, %v", got, err) + } + if filepath.Base(downloadedArchive) != assetName { + t.Fatalf("downloaded archive = %q, want basename %q", downloadedArchive, assetName) + } + }) } - if filepath.Base(downloadedArchive) != assetName { - t.Fatalf("downloaded archive = %q, want basename %q", downloadedArchive, assetName) +} + +func TestReleaseDownloadURL(t *testing.T) { + for _, tc := range []struct{ version, repository, tag string }{ + {"1.2.3", "copilot-cli", "v1.2.3"}, + {"1.2.3-4", "copilot-cli", "v1.2.3-4"}, + {"1.2.3-unstable.r123.gabcdef0", "copilot-sdk", "runtime-1.2.3-unstable.r123.gabcdef0"}, + {"1.2.3-4.unstable.r123.gabcdef0", "copilot-sdk", "runtime-1.2.3-4.unstable.r123.gabcdef0"}, + {"0.0.0-0.unstable.r1.g0000000", "copilot-sdk", "runtime-0.0.0-0.unstable.r1.g0000000"}, + {"01.2.3-unstable.r123.gabcdef0", "copilot-cli", "v01.2.3-unstable.r123.gabcdef0"}, + {"1.02.3-unstable.r123.gabcdef0", "copilot-cli", "v1.02.3-unstable.r123.gabcdef0"}, + {"1.2.03-unstable.r123.gabcdef0", "copilot-cli", "v1.2.03-unstable.r123.gabcdef0"}, + {"1.2.3-04.unstable.r123.gabcdef0", "copilot-cli", "v1.2.3-04.unstable.r123.gabcdef0"}, + } { + t.Run(tc.version, func(t *testing.T) { + t.Setenv(cliDownloadBaseURLEnvironment, "") + for _, asset := range []string{"SHA256SUMS.txt", releaseAssetName(tc.version, "linux-x64")} { + want := "https://github.com/github/" + tc.repository + "/releases/download/" + tc.tag + "/" + asset + if got := releaseDownloadURL(tc.version, asset); got != want { + t.Fatalf("releaseDownloadURL() = %q, want %q", got, want) + } + } + t.Setenv(cliDownloadBaseURLEnvironment, "https://mirror.example/releases/") + want := "https://mirror.example/releases/" + tc.tag + "/SHA256SUMS.txt" + if got := releaseDownloadURL(tc.version, "SHA256SUMS.txt"); got != want { + t.Fatalf("mirror URL = %q, want %q", got, want) + } + }) } } diff --git a/go/connectors_rpc_test.go b/go/connectors_rpc_test.go index ff92a1b29a..41962b972d 100644 --- a/go/connectors_rpc_test.go +++ b/go/connectors_rpc_test.go @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + package copilot import ( @@ -82,6 +84,91 @@ func TestSessionRPCConnectors(t *testing.T) { !result.OpaqueAccountSelection { t.Fatalf("GetCapabilities result = %#v", result) } + if result.SessionAccountSelection != nil || result.TargetedReconcile != nil { + t.Fatalf("legacy capabilities must not enable new operations: %#v", result) + } + }) + + t.Run("session account", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getCapabilities", map[string]any{ + "sessionId": "session-1", + }, `{ + "apiVersion":1,"availability":"enabled","consentContinuation":true, + "maxDeadlineMs":60000,"maxPollAttempts":10,"maxPollIntervalMs":5000, + "opaqueAccountSelection":true,"sessionAccountSelection":true,"targetedReconcile":true + }`) + capabilities, err := session.RPC.Connectors.GetCapabilities(t.Context()) + if err != nil { + t.Fatalf("GetCapabilities: %v", err) + } + if capabilities.SessionAccountSelection == nil || !*capabilities.SessionAccountSelection || + capabilities.TargetedReconcile == nil || !*capabilities.TargetedReconcile { + t.Fatalf("new capabilities not enabled: %#v", capabilities) + } + + const account = `{"accountId":"` + connectorAccountID + `","authInfo":{"type":"token","host":"github.com","login":"alice"}}` + expectConnectorRPC(t, server, "session.connectors.getAccount", map[string]any{ + "sessionId": "session-1", + }, account) + result, err := session.RPC.Connectors.GetAccount(t.Context()) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if result == nil || result.AccountID != connectorAccountID || result.AuthInfo.Type != "token" || + result.AuthInfo.Host != "github.com" || result.AuthInfo.Login != "alice" { + t.Fatalf("GetAccount result = %#v", result) + } + wire, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + var got, want any + if err := json.Unmarshal(wire, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(account), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("credential-free account wire = %s", wire) + } + }) + + t.Run("unavailable session account is nil", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getAccount", map[string]any{ + "sessionId": "session-1", + }, `null`) + result, err := session.RPC.Connectors.GetAccount(t.Context()) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if result != nil { + t.Fatalf("null must not become an empty account: %#v", result) + } + // The exported result type models the schema's null branch, so it must + // accept GetAccount's return and a nil absent account. + exported := rpc.ConnectorSessionAccountResult(result) + if exported != nil { + t.Fatalf("exported result type must hold the absent account: %#v", exported) + } + }) + + t.Run("false capabilities remain unsupported", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getCapabilities", map[string]any{ + "sessionId": "session-1", + }, `{ + "apiVersion":1,"availability":"enabled","consentContinuation":true, + "maxDeadlineMs":60000,"maxPollAttempts":10,"maxPollIntervalMs":5000, + "opaqueAccountSelection":true,"sessionAccountSelection":false,"targetedReconcile":false + }`) + result, err := session.RPC.Connectors.GetCapabilities(t.Context()) + if err != nil { + t.Fatalf("GetCapabilities: %v", err) + } + if result.SessionAccountSelection == nil || *result.SessionAccountSelection || + result.TargetedReconcile == nil || *result.TargetedReconcile { + t.Fatalf("false capabilities must stay false: %#v", result) + } }) t.Run("get status", func(t *testing.T) { @@ -143,6 +230,33 @@ func TestSessionRPCConnectors(t *testing.T) { t.Fatalf("Refresh: %v", err) } assertConnectorCatalog(t, result, 8, rpc.ConnectorCatalogStatusNotConnected) + if result.Connectors[0].Logo != nil || result.Connectors[0].Tier != nil || result.Connectors[0].ReleaseTag != nil { + t.Fatalf("legacy catalog presentation metadata must stay absent: %#v", result.Connectors[0]) + } + }) + + t.Run("catalog presentation metadata", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.list", map[string]any{ + "accountId": connectorAccountID, + "sessionId": "session-1", + }, `{ + "connectors":[{ + "displayName":"Outlook","name":"outlook","runtimeServerIds":[],"status":"connected", + "logo":"https://example.com/outlook.svg","tier":"standard","releaseTag":"preview" + }], + "refreshedAtMs":1700000000100,"revision":8 + }`) + result, err := session.RPC.Connectors.List(t.Context(), &rpc.ConnectorAccountRequest{AccountID: connectorAccountID}) + if err != nil { + t.Fatalf("List: %v", err) + } + assertConnectorCatalog(t, result, 8, rpc.ConnectorCatalogStatusConnected) + entry := result.Connectors[0] + if entry.Logo == nil || *entry.Logo != "https://example.com/outlook.svg" || + entry.Tier == nil || *entry.Tier != "standard" || + entry.ReleaseTag == nil || *entry.ReleaseTag != "preview" { + t.Fatalf("catalog presentation metadata = %#v", entry) + } }) t.Run("connect", func(t *testing.T) { @@ -214,6 +328,33 @@ func TestSessionRPCConnectors(t *testing.T) { assertConnectorStatus(t, &connected.Status) }) + t.Run("unknown continuation outcome is not connected", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.continueConnection", map[string]any{ + "continuationId": "continuation-2", + "deadlineMs": float64(30000), + "maxAttempts": float64(4), + "pollIntervalMs": float64(500), + "sessionId": "session-1", + }, `{"kind":"future_outcome","continuationId":"continuation-2","status":`+connectorStatusResult+`}`) + + result, err := session.RPC.Connectors.ContinueConnection(t.Context(), &rpc.ConnectorContinueRequest{ + ContinuationID: "continuation-2", + DeadlineMs: 30000, + MaxAttempts: 4, + PollIntervalMs: 500, + }) + if err != nil { + t.Fatalf("ContinueConnection: %v", err) + } + raw, ok := result.(*rpc.RawConnectorConnectResultData) + if !ok || raw.Kind() != "future_outcome" { + t.Fatalf("unknown outcome must remain raw, got %#v", result) + } + if _, connected := result.(*rpc.ConnectorConnectResultConnected); connected { + t.Fatal("unknown outcome was treated as connected") + } + }) + t.Run("disconnect", func(t *testing.T) { expectConnectorRPC(t, server, "session.connectors.disconnect", map[string]any{ "accountId": connectorAccountID, @@ -250,6 +391,25 @@ func TestSessionRPCConnectors(t *testing.T) { } assertConnectorStatus(t, result) }) + + t.Run("targeted reconcile", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.reconcile", map[string]any{ + "accountId": connectorAccountID, + "refreshCatalog": true, + "forceConnectorName": "outlook", + "sessionId": "session-1", + }, connectorStatusResult) + + result, err := session.RPC.Connectors.Reconcile(t.Context(), &rpc.ConnectorReconcileRequest{ + AccountID: connectorAccountID, + RefreshCatalog: Bool(true), + ForceConnectorName: String("outlook"), + }) + if err != nil { + t.Fatalf("Reconcile: %v", err) + } + assertConnectorStatus(t, result) + }) } func expectConnectorRPC(t *testing.T, server *jsonrpc2.Client, method string, wantParams map[string]any, result string) { diff --git a/go/internal/e2e/compaction_e2e_test.go b/go/internal/e2e/compaction_e2e_test.go index e994cdfc7e..9bf2084e52 100644 --- a/go/internal/e2e/compaction_e2e_test.go +++ b/go/internal/e2e/compaction_e2e_test.go @@ -2,6 +2,7 @@ package e2e import ( "errors" + "os" "strings" "testing" "time" @@ -118,8 +119,40 @@ func TestCompactionE2E(t *testing.T) { } if completeData.CompactionTokensUsed == nil { t.Errorf("Expected compaction tokens-used data") - } else if completeData.CompactionTokensUsed.InputTokens == nil || *completeData.CompactionTokensUsed.InputTokens <= 0 { - t.Errorf("Expected compaction call to consume input tokens, got %v", completeData.CompactionTokensUsed.InputTokens) + } else { + actualInputTokens := completeData.CompactionTokensUsed.InputTokens + // Replay has no prompt usage; recording must preserve the provider's value or absence. + if os.Getenv("GITHUB_ACTIONS") == "true" { + if actualInputTokens != nil && *actualInputTokens != 0 { + t.Errorf("Expected replay compaction input tokens to be zero or unavailable, got %d", *actualInputTokens) + } + } else { + var expectedInputTokens *int64 + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("Failed to get provider responses: %v", err) + } + var compactions []*testharness.CompactionProviderUsage + for _, exchange := range exchanges { + if exchange.CompactionUsage != nil && + completeData.SummaryContent != nil && exchange.CompactionUsage.Summary == *completeData.SummaryContent { + compactions = append(compactions, exchange.CompactionUsage) + } + } + if len(compactions) != 1 { + t.Fatalf("Expected one provider compaction chain for the completed summary, got %d", len(compactions)) + } + expectedInputTokens = compactions[0].InputTokens + if expectedInputTokens == nil { + if actualInputTokens != nil { + t.Errorf("Expected provider input tokens to be unavailable, got %d", *actualInputTokens) + } + } else if actualInputTokens == nil { + t.Errorf("Expected provider input tokens %d, got unavailable", *expectedInputTokens) + } else if *actualInputTokens != *expectedInputTokens { + t.Errorf("Expected provider input tokens %d, got %d", *expectedInputTokens, *actualInputTokens) + } + } } summary := "" if completeData.SummaryContent != nil { diff --git a/go/internal/e2e/event_fidelity_e2e_test.go b/go/internal/e2e/event_fidelity_e2e_test.go index 8606da22d9..e2b7226f5b 100644 --- a/go/internal/e2e/event_fidelity_e2e_test.go +++ b/go/internal/e2e/event_fidelity_e2e_test.go @@ -1,11 +1,13 @@ package e2e import ( + "context" "os" "path/filepath" "strings" "sync" "testing" + "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" @@ -134,41 +136,39 @@ func TestEventFidelityE2E(t *testing.T) { } t.Cleanup(func() { _ = session.Disconnect() }) - var mu sync.Mutex - var events []copilot.SessionEvent - session.On(func(event copilot.SessionEvent) { - mu.Lock() - events = append(events, event) - mu.Unlock() - }) + // pending_messages.modified and session.idle are independent notifications. + // Subscribe to both before Send so either delivery order is retained. + pendingModified := testharness.SubscribeToEvent( + session, + copilot.SessionEventTypePendingMessagesModified, + ) + finalAssistant := testharness.SubscribeToFinalAssistantMessage(session) + t.Cleanup(pendingModified.Close) + t.Cleanup(finalAssistant.Close) + + waitCtx, cancel := context.WithTimeout(t.Context(), 60*time.Second) + defer cancel() - // SendAndWait collects everything in one round trip and matches the - // pattern of every other test in this file (and the Rust E2E equivalent), - // avoiding the split fire-and-forget + helper pattern that previously - // made this test prone to flakes. - answer, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + if _, err := session.Send(waitCtx, copilot.MessageOptions{ Prompt: "What is 9+9? Reply with just the number.", - }) - if err != nil { - t.Fatalf("SendAndWait failed: %v", err) + }); err != nil { + t.Fatalf("Send failed: %v", err) } - snapshot := snapshotEventFidelityEvents(&mu, &events) - - var pendingEvent *copilot.SessionEvent - for i := range snapshot { - if _, ok := snapshot[i].Data.(*copilot.PendingMessagesModifiedData); ok { - pendingEvent = &snapshot[i] - break - } + pendingEvent, err := pendingModified.Wait(waitCtx) + if err != nil { + t.Fatalf("Failed waiting for pending_messages.modified: %v", err) } if pendingEvent == nil { - t.Error("Expected to observe a pending_messages.modified event") + t.Fatal("Expected a pending_messages.modified event") + } + if _, ok := pendingEvent.Data.(*copilot.PendingMessagesModifiedData); !ok { + t.Fatalf("Expected pending_messages.modified data, got %T", pendingEvent.Data) } - if answer == nil { - t.Fatal("Expected SendAndWait to return an assistant message") - return + answer, err := finalAssistant.Wait(waitCtx) + if err != nil { + t.Fatalf("Failed waiting for final assistant message: %v", err) } if ad, ok := answer.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(ad.Content, "18") { t.Errorf("Expected answer to contain '18', got %v", answer.Data) diff --git a/go/internal/e2e/hooks_extended_e2e_test.go b/go/internal/e2e/hooks_extended_e2e_test.go index 5cbba38566..2c804eba99 100644 --- a/go/internal/e2e/hooks_extended_e2e_test.go +++ b/go/internal/e2e/hooks_extended_e2e_test.go @@ -13,13 +13,13 @@ import ( // Mirrors dotnet/test/HookLifecycleAndOutputTests.cs (snapshot category "hooks_extended"). // -// Covers each handler exposed on copilot.SessionHooks: OnPreToolUse, +// Covers the general-purpose handlers on copilot.SessionHooks: OnPreToolUse, // OnPostToolUse, OnPostToolUseFailure, OnUserPromptSubmitted, // OnUserPromptTransformed, OnSessionStart, OnSessionEnd, OnErrorOccurred, -// OnAgentStop. Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt / +// OnAgentStop. Subagent hooks are exercised in subagent_hooks_e2e_test.go. +// Output-shape behavior (modifiedPrompt / modifiedTransformedPrompt / // additionalContext / errorHandling / modifiedArgs / modifiedResult / -// sessionSummary) is asserted alongside hook invocation. If a new handler is -// added to SessionHooks, add a corresponding test here. +// sessionSummary) is asserted alongside hook invocation. func TestHooksExtendedE2E(t *testing.T) { ctx := testharness.NewTestContext(t) client := ctx.NewClient() diff --git a/go/internal/e2e/session_fs_e2e_test.go b/go/internal/e2e/session_fs_e2e_test.go index 4236e8e529..18d41a46b5 100644 --- a/go/internal/e2e/session_fs_e2e_test.go +++ b/go/internal/e2e/session_fs_e2e_test.go @@ -1,6 +1,7 @@ package e2e import ( + "encoding/base64" "fmt" "os" "path/filepath" @@ -76,6 +77,84 @@ func TestSessionFSE2E(t *testing.T) { } }) + t.Run("should view an image that exists only in the binary session fs provider", func(t *testing.T) { + ctx.ConfigureForTest(t) + + imagePath := "/sdk-provider-image.png" + imageBytes, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==") + if err != nil { + t.Fatal(err) + } + binaryConfig := *sessionFSConfig + binaryConfig.Capabilities = &copilot.SessionFSCapabilities{Binary: true} + binaryClient := ctx.NewClient(func(opts *copilot.ClientOptions) { + opts.SessionFS = &binaryConfig + }) + t.Cleanup(func() { binaryClient.ForceStop() }) + readPaths := make(chan string, 1) + session, err := binaryClient.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CreateSessionFSProvider: func(session *copilot.Session) copilot.SessionFSProvider { + return &testBinarySessionFSHandler{ + testSessionFSHandler: &testSessionFSHandler{root: providerRoot, sessionID: session.SessionID}, + readPaths: readPaths, + } + }, + }) + if err != nil { + t.Fatalf("Failed to create binary session: %v", err) + } + storedPath := p(session.SessionID, imagePath) + if err := os.MkdirAll(filepath.Dir(storedPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(storedPath, imageBytes, 0o644); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(imagePath); !os.IsNotExist(err) { + t.Fatalf("Image unexpectedly exists on the runtime filesystem: %v", err) + } + + msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + }) + if err != nil { + t.Fatalf("Failed to view provider image: %v", err) + } + if msg == nil { + t.Fatal("Expected a final assistant response") + } + response, ok := msg.Data.(*copilot.AssistantMessageData) + if !ok || !strings.Contains(response.Content, "SDK_PROVIDER_IMAGE_DONE") { + t.Fatalf("Expected final image response, got %#v", msg) + } + select { + case path := <-readPaths: + if path != imagePath { + t.Fatalf("Expected binary read of %q, got %q", imagePath, path) + } + default: + t.Fatal("Expected a binary read from the provider") + } + events, err := session.GetEvents(t.Context()) + if err != nil { + t.Fatalf("Failed to read session events: %v", err) + } + foundAsset := false + for _, event := range events { + if asset, ok := event.Data.(*copilot.SessionBinaryAssetData); ok && + asset.MIMEType == "image/png" && asset.Data == base64.StdEncoding.EncodeToString(imageBytes) { + foundAsset = true + } + } + if !foundAsset { + t.Fatal("Expected a binary asset with the exact provider image bytes") + } + if err := session.Disconnect(); err != nil { + t.Fatalf("Failed to disconnect session: %v", err) + } + }) + t.Run("should load session data from fs provider on resume", func(t *testing.T) { ctx.ConfigureForTest(t) @@ -346,6 +425,31 @@ type testSessionFSHandler struct { sessionID string } +type testBinarySessionFSHandler struct { + *testSessionFSHandler + readPaths chan<- string +} + +func (h *testBinarySessionFSHandler) ReadFileBytes(path string) ([]byte, error) { + select { + case h.readPaths <- path: + default: + } + return os.ReadFile(providerPath(h.root, h.sessionID, path)) +} + +func (h *testBinarySessionFSHandler) WriteFileBytes(path string, content []byte, mode *int) error { + fullPath := providerPath(h.root, h.sessionID, path) + if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil { + return err + } + perm := os.FileMode(0o666) + if mode != nil { + perm = os.FileMode(*mode) + } + return os.WriteFile(fullPath, content, perm) +} + func (h *testSessionFSHandler) ReadFile(path string) (string, error) { content, err := os.ReadFile(providerPath(h.root, h.sessionID, path)) if err != nil { diff --git a/go/internal/e2e/set_tools_e2e_test.go b/go/internal/e2e/set_tools_e2e_test.go new file mode 100644 index 0000000000..75b818fa9d --- /dev/null +++ b/go/internal/e2e/set_tools_e2e_test.go @@ -0,0 +1,311 @@ +package e2e + +import ( + "fmt" + "slices" + "strings" + "sync" + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +const ( + fruitPrompt = "Use lookup_fruit to find the fruit for code 42." + fruitAndVegetablePrompt = "Use lookup_fruit to find the fruit for code 42 again, and use lookup_vegetable to find the vegetable for code 7." + vegetablePrompt = "Use lookup_vegetable to find the vegetable for code 7." +) + +func TestSetToolsE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(client.ForceStop) + + t.Run("replaces_tools_on_a_created_session", func(t *testing.T) { + ctx.ConfigureSnapshot(t, "set_tools/replaces_tools_on_a_created_session") + var originalLookups, replacementLookups, vegetableLookups toolCallRecorder + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{ + lookupFruitTool("apple", &originalLookups), + retiredLookupTool(), + }, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + first, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitPrompt}) + if err != nil { + t.Fatalf("first SendAndWait failed: %v", err) + } + assertAssistantContains(t, first, "apple") + + if err := session.SetTools(t.Context(), []copilot.Tool{ + lookupFruitTool("dragonfruit", &replacementLookups), + lookupVegetableTool(&vegetableLookups), + }); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + + second, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitAndVegetablePrompt}) + if err != nil { + t.Fatalf("second SendAndWait failed: %v", err) + } + assertAssistantContains(t, second, "dragonfruit") + assertAssistantContains(t, second, "carrot") + assertCalls(t, "original fruit lookups", originalLookups.calls(), []int{42}) + assertCalls(t, "replacement fruit lookups", replacementLookups.calls(), []int{42}) + assertCalls(t, "vegetable lookups", vegetableLookups.calls(), []int{7}) + + // Model requests after the replacement offer exactly the new tool set. + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("GetExchanges failed: %v", err) + } + replacedFrom := indexOfPrompt(exchanges, fruitAndVegetablePrompt) + if replacedFrom <= 0 { + t.Fatalf("replacement prompt first sent in exchange %d, want an earlier exchange before it", replacedFrom) + } + assertOffered(t, exchanges[:replacedFrom], []string{"lookup_fruit", "retired_lookup"}, []string{"lookup_vegetable"}) + assertOffered(t, exchanges[replacedFrom:], []string{"lookup_fruit", "lookup_vegetable"}, []string{"retired_lookup"}) + }) + + t.Run("replaces_tools_on_a_resumed_session", func(t *testing.T) { + ctx.ConfigureSnapshot(t, "set_tools/replaces_tools_on_a_resumed_session") + var createdLookups toolCallRecorder + created, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{lookupFruitTool("apple", &createdLookups)}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + sessionID := created.SessionID + first, err := created.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitPrompt}) + if err != nil { + t.Fatalf("first SendAndWait failed: %v", err) + } + assertAssistantContains(t, first, "apple") + assertCalls(t, "created fruit lookups", createdLookups.calls(), []int{42}) + if err := created.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + + var fruitLookups, vegetableLookups toolCallRecorder + resumed, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{lookupFruitTool("apple", &fruitLookups)}, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = resumed.Disconnect() }) + if err := resumed.SetTools(t.Context(), []copilot.Tool{lookupVegetableTool(&vegetableLookups)}); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + + answer, err := resumed.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: vegetablePrompt}) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + assertAssistantContains(t, answer, "carrot") + assertCalls(t, "vegetable lookups", vegetableLookups.calls(), []int{7}) + assertCalls(t, "fruit lookups", fruitLookups.calls(), nil) + + exchanges, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("GetExchanges failed: %v", err) + } + replacedFrom := indexOfPrompt(exchanges, vegetablePrompt) + if replacedFrom <= 0 { + t.Fatalf("replacement prompt first sent in exchange %d, want an earlier exchange before it", replacedFrom) + } + assertOffered(t, exchanges[replacedFrom:], []string{"lookup_vegetable"}, []string{"lookup_fruit"}) + }) + + t.Run("keeps_the_previous_tools_when_a_replacement_is_rejected", func(t *testing.T) { + ctx.ConfigureSnapshot(t, "set_tools/keeps_the_previous_tools_when_a_replacement_is_rejected") + var originalLookups, replacementLookups toolCallRecorder + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{lookupFruitTool("apple", &originalLookups)}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + err = session.SetTools(t.Context(), []copilot.Tool{ + lookupFruitTool("dragonfruit", &replacementLookups), + invalidTool(), + }) + if err == nil { + t.Fatal("SetTools succeeded; want runtime rejection") + } + + answer, err := session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: fruitPrompt}) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + assertAssistantContains(t, answer, "apple") + assertCalls(t, "original fruit lookups", originalLookups.calls(), []int{42}) + assertCalls(t, "replacement fruit lookups", replacementLookups.calls(), nil) + }) +} + +type toolCallRecorder struct { + mu sync.Mutex + codes []int +} + +func (r *toolCallRecorder) record(code int) { + if r == nil { + return + } + r.mu.Lock() + defer r.mu.Unlock() + r.codes = append(r.codes, code) +} + +func (r *toolCallRecorder) calls() []int { + r.mu.Lock() + defer r.mu.Unlock() + return append([]int(nil), r.codes...) +} + +func lookupFruitTool(fruit string, calls *toolCallRecorder) copilot.Tool { + return numericCodeTool("lookup_fruit", "Looks up the fruit for a numeric code", "Fruit code", func(code int) string { + calls.record(code) + return fruit + }) +} + +func lookupVegetableTool(calls *toolCallRecorder) copilot.Tool { + return numericCodeTool("lookup_vegetable", "Looks up the vegetable for a numeric code", "Vegetable code", func(code int) string { + calls.record(code) + return "carrot" + }) +} + +func retiredLookupTool() copilot.Tool { + return copilot.Tool{ + Name: "retired_lookup", + Description: "Looks up a retired value", + Handler: func(_ copilot.ToolInvocation) (copilot.ToolResult, error) { + return copilot.ToolResult{TextResultForLLM: "retired"}, nil + }, + } +} + +func invalidTool() copilot.Tool { + return copilot.Tool{ + Name: "invalid.tool", + Description: "Has a name the runtime rejects", + Handler: func(_ copilot.ToolInvocation) (copilot.ToolResult, error) { + return copilot.ToolResult{TextResultForLLM: "never"}, nil + }, + } +} + +func numericCodeTool(name, description, codeDescription string, handler func(int) string) copilot.Tool { + return copilot.Tool{ + Name: name, + Description: description, + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "code": map[string]any{ + "type": "integer", + "description": codeDescription, + }, + }, + "required": []string{"code"}, + }, + Handler: func(inv copilot.ToolInvocation) (copilot.ToolResult, error) { + code, err := codeArgument(inv.Arguments) + if err != nil { + return copilot.ToolResult{}, err + } + return copilot.ToolResult{TextResultForLLM: handler(code)}, nil + }, + } +} + +func codeArgument(arguments any) (int, error) { + args, ok := arguments.(map[string]any) + if !ok { + return 0, fmt.Errorf("arguments = %#v, want object", arguments) + } + switch code := args["code"].(type) { + case float64: + return int(code), nil + case int: + return code, nil + default: + return 0, fmt.Errorf("code argument = %#v, want number", args["code"]) + } +} + +func assertAssistantContains(t *testing.T, event *copilot.SessionEvent, want string) { + t.Helper() + if event == nil { + t.Fatalf("assistant response is nil, want content containing %q", want) + } + data, ok := event.Data.(*copilot.AssistantMessageData) + if !ok { + t.Fatalf("event data = %T, want AssistantMessageData", event.Data) + } + if !strings.Contains(data.Content, want) { + t.Fatalf("assistant response = %q, want content containing %q", data.Content, want) + } +} + +func assertCalls(t *testing.T, label string, got, want []int) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("%s = %v, want %v", label, got, want) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("%s = %v, want %v", label, got, want) + } + } +} + +// indexOfPrompt returns the index of the first model request that carries +// prompt as a user message, or -1. +func indexOfPrompt(exchanges []testharness.ParsedHttpExchange, prompt string) int { + for i, exchange := range exchanges { + for _, message := range exchange.Request.Messages { + if message.Role == "user" && (strings.Contains(message.Content, prompt) || strings.Contains(string(message.RawContent), prompt)) { + return i + } + } + } + return -1 +} + +// assertOffered checks that every exchange offered the tools in offered and +// none of the tools in notOffered. +func assertOffered(t *testing.T, exchanges []testharness.ParsedHttpExchange, offered, notOffered []string) { + t.Helper() + for _, exchange := range exchanges { + tools := make([]string, 0, len(exchange.Request.Tools)) + for _, tool := range exchange.Request.Tools { + tools = append(tools, tool.Function.Name) + } + for _, name := range offered { + if !slices.Contains(tools, name) { + t.Fatalf("model request offered %v, want it to include %q", tools, name) + } + } + for _, name := range notOffered { + if slices.Contains(tools, name) { + t.Fatalf("model request offered %v, want it to omit %q", tools, name) + } + } + } +} diff --git a/go/internal/e2e/skill_provider_e2e_test.go b/go/internal/e2e/skill_provider_e2e_test.go new file mode 100644 index 0000000000..d6c2dc93fb --- /dev/null +++ b/go/internal/e2e/skill_provider_e2e_test.go @@ -0,0 +1,586 @@ +package e2e + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +type providedSkill struct { + descriptor rpc.SkillProviderDescriptor + read func() (string, error) +} + +type testSkillProvider struct { + mu sync.Mutex + skills []providedSkill + calls []string +} + +func newTestSkillProvider(skills []providedSkill) *testSkillProvider { + return &testSkillProvider{skills: skills} +} + +func providedSkillWithMarkdown(name, description, markdown string) providedSkill { + return providedSkill{ + descriptor: rpc.SkillProviderDescriptor{Name: name, Description: description}, + read: func() (string, error) { return markdown, nil }, + } +} + +func (p *testSkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "list") + skills := make([]rpc.SkillProviderDescriptor, 0, len(p.skills)) + for _, skill := range p.skills { + skills = append(skills, skill.descriptor) + } + return skills, nil +} + +func (p *testSkillProvider) ReadSkill(ctx context.Context, name string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "read:"+name) + for _, skill := range p.skills { + if skill.descriptor.Name == name { + return skill.read() + } + } + return "", copilot.ErrSkillNotFound +} + +func (p *testSkillProvider) Calls() []string { + p.mu.Lock() + defer p.mu.Unlock() + calls := make([]string, len(p.calls)) + copy(calls, p.calls) + return calls +} + +func (p *testSkillProvider) Reads() []string { + calls := p.Calls() + reads := make([]string, 0, len(calls)) + for _, call := range calls { + if strings.HasPrefix(call, "read:") { + reads = append(reads, strings.TrimPrefix(call, "read:")) + } + } + return reads +} + +// blockingSkillProvider blocks ListSkills until its context is cancelled. +type blockingSkillProvider struct { + entered chan struct{} + cancelled chan struct{} + enterOnce sync.Once + cancelOnce sync.Once +} + +func newBlockingSkillProvider() *blockingSkillProvider { + return &blockingSkillProvider{entered: make(chan struct{}), cancelled: make(chan struct{})} +} + +func (p *blockingSkillProvider) ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.enterOnce.Do(func() { close(p.entered) }) + <-ctx.Done() + p.cancelOnce.Do(func() { close(p.cancelled) }) + return nil, ctx.Err() +} + +func (p *blockingSkillProvider) ReadSkill(context.Context, string) (string, error) { + return "", copilot.ErrSkillNotFound +} + +func TestSkillProviderE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + t.Run("should load provider skill lazily through skill tool", func(t *testing.T) { + ctx.ConfigureForTest(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n", + ), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + listed := findSkill(list, "provider-lookup") + if listed == nil { + t.Fatal("Expected provider-lookup skill to be listed") + } + if listed.Source != rpc.SkillSourceSDK || !listed.Enabled { + t.Fatalf("Expected provider-lookup source=sdk enabled=true, got %+v", listed) + } + if listed.Path != nil && *listed.Path != "" { + t.Fatalf("Expected provider-lookup path to be empty, got %q", *listed.Path) + } + if reads := provider.Reads(); len(reads) != 0 { + t.Fatalf("Provider reads before skill load = %v, want []", reads) + } + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, provider.Reads(), []string{"provider-lookup"}) + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "TANGERINE_QUARTZ_19") + }) + + t.Run("should load provider and file based skills together", func(t *testing.T) { + ctx.ConfigureForTest(t) + skillsDir := filepath.Join(ctx.WorkDir, "file-skills") + fileSkillDir := filepath.Join(skillsDir, "file-notes") + if err := os.MkdirAll(fileSkillDir, 0755); err != nil { + t.Fatalf("MkdirAll failed: %v", err) + } + if err := os.WriteFile( + filepath.Join(fileSkillDir, "SKILL.md"), + []byte("---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n"), + 0644, + ); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n", + ), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillDirectories: []string{skillsDir}, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + fileSkill := findSkill(list, "file-notes") + providerSkill := findSkill(list, "provider-audit") + if fileSkill == nil || fileSkill.Source == rpc.SkillSourceSDK || fileSkill.Path == nil || *fileSkill.Path == "" { + t.Fatalf("Unexpected file-notes skill: %+v", fileSkill) + } + if providerSkill == nil || providerSkill.Source != rpc.SkillSourceSDK { + t.Fatalf("Unexpected provider-audit skill: %+v", providerSkill) + } + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, provider.Reads(), []string{"provider-audit"}) + assertAssistantContains(t, message, "MAPLE_FALCON_27") + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "COBALT_HERON_58") + }) + + t.Run("should rebind skill provider on resume", func(t *testing.T) { + ctx.ConfigureForTest(t) + original := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n", + ), + }) + replacement := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n", + ), + }) + first, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: original, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + sessionID := first.SessionID + if _, err := first.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Without using any tools or skills, reply with exactly REBIND_READY.", + }); err != nil { + t.Fatalf("Initial SendAndWait failed: %v", err) + } + if err := first.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + if reads := original.Reads(); len(reads) != 0 { + t.Fatalf("Original provider reads before resume = %v, want []", reads) + } + originalCallsBeforeResume := len(original.Calls()) + + session, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: replacement, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertEqualStrings(t, replacement.Reads(), []string{"rebind-check"}) + if got := len(original.Calls()); got != originalCallsBeforeResume { + t.Fatalf("Original provider call count after resume = %d, want %d", got, originalCallsBeforeResume) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "BRONZE_BETA_22") + assertAssistantNotContains(t, message, "AMBER_ALPHA_11") + }) + + t.Run("should report provider read failure without leaking details", func(t *testing.T) { + ctx.ConfigureForTest(t) + secret := "PROVIDER_SECRET_7F3A9C" + provider := newTestSkillProvider([]providedSkill{ + { + descriptor: rpc.SkillProviderDescriptor{ + Name: "broken-lookup", + Description: "Reports the broken lookup verification word.", + }, + read: func() (string, error) { + return "", errors.New("database unavailable: " + secret) + }, + }, + }) + events := newEventRecorder() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + OnEvent: events.Record, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertContainsString(t, provider.Reads(), "broken-lookup") + failures := failedToolCompletions(events.Snapshot()) + if len(failures) != 1 { + t.Fatalf("Expected exactly 1 failed tool completion, got %d", len(failures)) + } + rawEvents, err := json.Marshal(events.Snapshot()) + if err != nil { + t.Fatalf("Marshal events failed: %v", err) + } + if strings.Contains(string(rawEvents), secret) { + t.Fatalf("Provider error leaked secret in events: %s", rawEvents) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "LOAD_FAILED") + }) + + t.Run("should report missing provider skill as not found", func(t *testing.T) { + ctx.ConfigureForTest(t) + provider := newTestSkillProvider([]providedSkill{ + { + descriptor: rpc.SkillProviderDescriptor{ + Name: "vanished-lookup", + Description: "Reports the vanished lookup verification word.", + }, + read: func() (string, error) { + return "", copilot.ErrSkillNotFound + }, + }, + }) + events := newEventRecorder() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + OnEvent: events.Record, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + }) + if err != nil { + t.Fatalf("SendAndWait failed: %v", err) + } + + assertContainsString(t, provider.Reads(), "vanished-lookup") + failures := failedToolCompletions(events.Snapshot()) + if len(failures) != 1 { + t.Fatalf("Expected exactly 1 failed tool completion, got %d", len(failures)) + } + rawFailure, err := json.Marshal(failures[0]) + if err != nil { + t.Fatalf("Marshal failure failed: %v", err) + } + if !strings.Contains(strings.ToLower(string(rawFailure)), "not found") { + t.Fatalf("Expected failure to mention not found, got %s", rawFailure) + } + // Validate the final assistant response arrived (guards against truncated captures) + assertAssistantContains(t, message, "LOAD_FAILED") + }) + + t.Run("should keep provider dormant when skills disabled", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("dormant-lookup", "Never listed.", "Never read.\n"), + }) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + EnableSkills: copilot.Bool(false), + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + if _, err := session.RPC.Skills.EnsureLoaded(t.Context()); err != nil { + t.Fatalf("Skills.EnsureLoaded failed: %v", err) + } + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List failed: %v", err) + } + if sdkSkills := sdkSkills(list); len(sdkSkills) != 0 { + t.Fatalf("Expected no sdk skills when disabled, got %+v", sdkSkills) + } + if calls := provider.Calls(); len(calls) != 0 { + t.Fatalf("Provider calls when skills disabled = %v, want []", calls) + } + }) + + t.Run("should unbind provider when resumed without one", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("unbound-lookup", "Reports the unbound lookup word.", "Unbound.\n"), + }) + first, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + before, err := first.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List before resume failed: %v", err) + } + if findSkill(before, "unbound-lookup") == nil { + t.Fatal("Expected unbound-lookup before resume") + } + callsBeforeResume := len(provider.Calls()) + + session, err := client.ResumeSession(t.Context(), first.SessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + if _, err := session.RPC.Skills.Reload(t.Context()); err != nil { + t.Fatalf("Skills.Reload failed: %v", err) + } + list, err := session.RPC.Skills.List(t.Context()) + if err != nil { + t.Fatalf("Skills.List after resume failed: %v", err) + } + if sdkSkills := sdkSkills(list); len(sdkSkills) != 0 { + t.Fatalf("Expected no sdk skills after unbound resume, got %+v", sdkSkills) + } + if got := len(provider.Calls()); got != callsBeforeResume { + t.Fatalf("Provider call count after unbound resume = %d, want %d", got, callsBeforeResume) + } + }) + + t.Run("should cancel a blocked provider call when the session disconnects", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newBlockingSkillProvider() + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + go func() { _, _ = session.RPC.Skills.List(context.Background()) }() + select { + case <-provider.entered: + case <-time.After(30 * time.Second): + t.Fatal("provider ListSkills was not called") + } + + if err := session.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + select { + case <-provider.cancelled: + case <-time.After(10 * time.Second): + t.Fatal("provider context was not cancelled after Disconnect") + } + }) + + t.Run("should reject skill provider for cloud sessions", func(t *testing.T) { + ctx.ConfigureWithoutSnapshot(t) + provider := newTestSkillProvider([]providedSkill{ + providedSkillWithMarkdown("cloud-lookup", "Never listed.", "Never read.\n"), + }) + + _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Cloud: &copilot.CloudSessionOptions{}, + SkillProvider: provider, + }) + if err == nil || err.Error() != "Skill providers are not supported for cloud sessions." { + t.Fatalf("CreateSession error = %v, want cloud skill provider rejection", err) + } + if calls := provider.Calls(); len(calls) != 0 { + t.Fatalf("Provider calls after cloud rejection = %v, want []", calls) + } + }) +} + +type eventRecorder struct { + mu sync.Mutex + events []copilot.SessionEvent +} + +func newEventRecorder() *eventRecorder { + return &eventRecorder{} +} + +func (r *eventRecorder) Record(event copilot.SessionEvent) { + r.mu.Lock() + defer r.mu.Unlock() + r.events = append(r.events, event) +} + +func (r *eventRecorder) Snapshot() []copilot.SessionEvent { + r.mu.Lock() + defer r.mu.Unlock() + events := make([]copilot.SessionEvent, len(r.events)) + copy(events, r.events) + return events +} + +func findSkill(list *rpc.SkillList, name string) *rpc.Skill { + if list == nil { + return nil + } + for i := range list.Skills { + if list.Skills[i].Name == name { + return &list.Skills[i] + } + } + return nil +} + +func sdkSkills(list *rpc.SkillList) []rpc.Skill { + if list == nil { + return nil + } + var matches []rpc.Skill + for _, skill := range list.Skills { + if skill.Source == rpc.SkillSourceSDK { + matches = append(matches, skill) + } + } + return matches +} + +func failedToolCompletions(events []copilot.SessionEvent) []*copilot.ToolExecutionCompleteData { + var failures []*copilot.ToolExecutionCompleteData + for _, event := range events { + if data, ok := event.Data.(*copilot.ToolExecutionCompleteData); ok && !data.Success { + failures = append(failures, data) + } + } + return failures +} + +func assertAssistantNotContains(t *testing.T, event *copilot.SessionEvent, text string) { + t.Helper() + data, ok := event.Data.(*copilot.AssistantMessageData) + if !ok { + t.Fatalf("Expected AssistantMessageData, got %T", event.Data) + } + if strings.Contains(data.Content, text) { + t.Fatalf("Expected assistant response not to contain %q, got %q", text, data.Content) + } +} + +func assertEqualStrings(t *testing.T, got, want []string) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("strings = %v, want %v", got, want) + } + for i := range got { + if got[i] != want[i] { + t.Fatalf("strings = %v, want %v", got, want) + } + } +} + +func assertContainsString(t *testing.T, got []string, want string) { + t.Helper() + for _, value := range got { + if value == want { + return + } + } + t.Fatalf("strings = %v, want to contain %q", got, want) +} diff --git a/go/internal/e2e/subagent_hooks_e2e_test.go b/go/internal/e2e/subagent_hooks_e2e_test.go index 2f2cedf904..f74cb6355d 100644 --- a/go/internal/e2e/subagent_hooks_e2e_test.go +++ b/go/internal/e2e/subagent_hooks_e2e_test.go @@ -1,37 +1,52 @@ package e2e import ( + "bytes" + "encoding/json" + "io" "net/http" "os" "path/filepath" + "strings" "sync" "testing" + "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" "github.com/google/uuid" ) +const childContext = "Subagent start hook verified: read the requested file." +const stopResponsePrefix = "Subagent stop hook verified: " + type subagentRequestRecord struct { agentID string parentAgentID string interactionType string + childPrompt string } type recordingForwardingTransport struct { - inner http.RoundTripper - mu sync.Mutex - records []subagentRequestRecord + inner http.RoundTripper + mu sync.Mutex + records []subagentRequestRecord + modifiedParentRequest chan struct{} } func newRecordingForwardingTransport() *recordingForwardingTransport { inner := http.DefaultTransport.(*http.Transport).Clone() inner.DisableCompression = true - return &recordingForwardingTransport{inner: inner} + return &recordingForwardingTransport{inner: inner, modifiedParentRequest: make(chan struct{}, 1)} } func (rt *recordingForwardingTransport) RoundTrip(req *http.Request) (*http.Response, error) { if isInferenceURL(req.URL.String()) { + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + req.Body = io.NopCloser(bytes.NewReader(body)) rctx := copilot.RequestContextFrom(req) record := subagentRequestRecord{} if rctx != nil { @@ -39,6 +54,50 @@ func (rt *recordingForwardingTransport) RoundTrip(req *http.Request) (*http.Resp record.parentAgentID = rctx.ParentAgentID record.interactionType = rctx.InteractionType } + if record.parentAgentID != "" { + var payload struct { + Messages []struct { + Role string `json:"role"` + Content json.RawMessage `json:"content"` + } `json:"messages"` + } + if err := json.Unmarshal(body, &payload); err != nil { + return nil, err + } + for _, message := range payload.Messages { + if message.Role == "user" && len(message.Content) > 0 { + var prompts []string + switch message.Content[0] { + case '"': + var prompt string + if err := json.Unmarshal(message.Content, &prompt); err != nil { + return nil, err + } + prompts = append(prompts, prompt) + case '[': + var parts []struct { + Text string `json:"text"` + } + if err := json.Unmarshal(message.Content, &parts); err != nil { + return nil, err + } + for _, part := range parts { + prompts = append(prompts, part.Text) + } + } + for _, prompt := range prompts { + if strings.Contains(prompt, childContext+"\n\n") { + record.childPrompt = prompt + } + } + } + } + } else if bytes.Contains(body, []byte(stopResponsePrefix)) { + select { + case rt.modifiedParentRequest <- struct{}{}: + default: + } + } rt.mu.Lock() rt.records = append(rt.records, record) rt.mu.Unlock() @@ -87,7 +146,7 @@ func TestSubagentHooksE2E(t *testing.T) { }) t.Cleanup(func() { client.ForceStop() }) - t.Run("should invoke preToolUse and postToolUse hooks for sub-agent tool calls", func(t *testing.T) { + t.Run("should apply subagent lifecycle hook outputs", func(t *testing.T) { ctx.ConfigureForTest(t) type hookEntry struct { @@ -103,6 +162,11 @@ func TestSubagentHooksE2E(t *testing.T) { parentWaiting := make(chan struct{}) releaseView := sync.OnceFunc(func() { close(parentWaiting) }) defer releaseView() + var startInputs []copilot.SubagentStartHookInput + var stopInputs []copilot.SubagentStopHookInput + var startInvocations []copilot.HookInvocation + var stopInvocations []copilot.HookInvocation + stopObserved := make(chan struct{}, 1) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ SessionID: parentSessionID, @@ -128,12 +192,29 @@ func TestSubagentHooksE2E(t *testing.T) { } return nil, nil }, + OnSubagentStart: func(input copilot.SubagentStartHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStartHookOutput, error) { + mu.Lock() + startInputs = append(startInputs, input) + startInvocations = append(startInvocations, invocation) + mu.Unlock() + return &copilot.SubagentStartHookOutput{AdditionalContext: childContext}, nil + }, + OnSubagentStop: func(input copilot.SubagentStopHookInput, invocation copilot.HookInvocation) (*copilot.SubagentStopHookOutput, error) { + mu.Lock() + stopInputs = append(stopInputs, input) + stopInvocations = append(stopInvocations, invocation) + mu.Unlock() + select { + case stopObserved <- struct{}{}: + default: + } + return &copilot.SubagentStopHookOutput{ModifiedResponse: copilot.String(stopResponsePrefix + input.Response)}, nil + }, }, }) if err != nil { t.Fatalf("Failed to create session: %v", err) } - // Create a file for the sub-agent to read testFile := filepath.Join(ctx.WorkDir, "subagent-test.txt") if err := os.WriteFile(testFile, []byte("Hello from subagent test!"), 0644); err != nil { @@ -173,8 +254,57 @@ func TestSubagentHooksE2E(t *testing.T) { t.Fatalf("Expected durable waiting reply followed by final reply, got %q", replies) } + select { + case <-stopObserved: + case <-time.After(120 * time.Second): + t.Fatal("Timed out waiting for the subagentStop hook") + } + select { + case <-transport.modifiedParentRequest: + case <-time.After(120 * time.Second): + t.Fatal("Timed out waiting for the parent inference request with the rewritten response") + } mu.Lock() defer mu.Unlock() + if len(startInputs) != 1 || len(stopInputs) != 1 { + t.Fatalf("Expected one subagentStart and one subagentStop invocation, got starts=%+v stops=%+v", startInputs, stopInputs) + } + start, stop := startInputs[0], stopInputs[0] + if startInvocations[0].SessionID != session.SessionID || stopInvocations[0].SessionID != session.SessionID || + start.SessionID != session.SessionID || stop.SessionID != session.SessionID { + t.Errorf("Expected parent session ID %q in hook inputs and invocations, got start=%+v stop=%+v", session.SessionID, start, stop) + } + if !start.Timestamp.After(time.UnixMilli(0)) || !stop.Timestamp.After(time.UnixMilli(0)) || + stop.Timestamp.Before(start.Timestamp) { + t.Errorf("Expected ordered timestamps, got start=%v stop=%v", start.Timestamp, stop.Timestamp) + } + if start.WorkingDirectory != ctx.WorkDir || stop.WorkingDirectory != ctx.WorkDir { + t.Errorf("Expected working directory %q, got start=%q stop=%q", ctx.WorkDir, start.WorkingDirectory, stop.WorkingDirectory) + } + if stop.TranscriptPath != start.TranscriptPath || (start.TranscriptPath != "" && !filepath.IsAbs(start.TranscriptPath)) { + t.Errorf("Expected matching transcript paths (absolute when available), got start=%q stop=%q", start.TranscriptPath, stop.TranscriptPath) + } + if start.AgentName != "explore" || stop.AgentName != start.AgentName || stop.AgentType != "explore" { + t.Errorf("Expected explore agent name and type, got start=%+v stop=%+v", start, stop) + } + if start.AgentDisplayName != "" { + t.Errorf("Expected no optional display name on this task-created agent, got %q", start.AgentDisplayName) + } + if stop.AgentDisplayName != start.AgentDisplayName { + t.Errorf("Expected stop display name %q, got %q", start.AgentDisplayName, stop.AgentDisplayName) + } + if start.AgentDescription != "" { + t.Errorf("Expected no optional description on this task-created agent, got %q", start.AgentDescription) + } + if stop.AgentDescription != start.AgentDescription { + t.Errorf("Expected stop description %q, got %q", start.AgentDescription, stop.AgentDescription) + } + if stop.AgentID == "" { + t.Error("Expected a runtime-generated sub-agent ID") + } + if stop.StopReason != "end_turn" || !strings.Contains(stop.Response, "Hello from subagent test!") { + t.Errorf("Expected completed turn and file contents in stop input, got %+v", stop) + } // Parent tool hooks fire for "task" var taskPre *hookEntry @@ -211,6 +341,16 @@ func TestSubagentHooksE2E(t *testing.T) { if viewPre[0].sessionID == taskPre.sessionID { t.Error("Sub-agent tool hooks should have a different sessionId than parent tool hooks") } - assertSubagentRequestMetadata(t, transport.inferenceRecords()) + requests := transport.inferenceRecords() + assertSubagentRequestMetadata(t, requests) + startContextObserved := false + for _, request := range requests { + if strings.Contains(request.childPrompt, childContext+"\n\nRead the file \"subagent-test.txt\"") { + startContextObserved = true + } + } + if !startContextObserved { + t.Errorf("Start hook context did not reach a child inference prompt: %+v", requests) + } }) } diff --git a/go/internal/e2e/testharness/proxy.go b/go/internal/e2e/testharness/proxy.go index e6a595ffe2..488215f8a6 100644 --- a/go/internal/e2e/testharness/proxy.go +++ b/go/internal/e2e/testharness/proxy.go @@ -154,10 +154,13 @@ func (p *CapiProxy) StopWithOptions(skipWritingCache bool) error { exited <- struct{}{} }() if !waitForProcessExit(exited, proxyShutdownTimeout) { - if err := killProcessTree(cmd); err != nil { - return fmt.Errorf("failed to kill proxy process: %w", err) - } + // Windows Wait can release the process handle before the kill attempt. + // A late kill error is harmless only when our own Wait confirms exit. + killErr := killProcessTree(cmd) if !waitForProcessExit(exited, proxyShutdownTimeout) { + if killErr != nil { + return fmt.Errorf("failed to kill proxy process: %w", killErr) + } return fmt.Errorf("proxy process did not exit after being killed") } } @@ -278,9 +281,18 @@ type CapturedRequest struct { // ParsedHttpExchange represents a captured HTTP exchange. type ParsedHttpExchange struct { - Request ChatCompletionRequest `json:"request"` - Response *ChatCompletionResponse `json:"response,omitempty"` - RequestHeaders map[string]json.RawMessage `json:"requestHeaders,omitempty"` + Request ChatCompletionRequest `json:"request"` + Response *ChatCompletionResponse `json:"response,omitempty"` + RequestHeaders map[string]json.RawMessage `json:"requestHeaders,omitempty"` + CompactionUsage *CompactionProviderUsage `json:"compactionUsage,omitempty"` +} + +// CompactionProviderUsage folds all responses in one correlated compaction request chain. +type CompactionProviderUsage struct { + InteractionID string `json:"interactionId"` + Summary string `json:"summary"` + ResponseCount int `json:"responseCount"` + InputTokens *int64 `json:"inputTokens,omitempty"` } // ChatCompletionRequest represents an OpenAI chat completion request. diff --git a/go/internal/e2e/testharness/proxy_test.go b/go/internal/e2e/testharness/proxy_test.go new file mode 100644 index 0000000000..cbd79c5548 --- /dev/null +++ b/go/internal/e2e/testharness/proxy_test.go @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package testharness + +import ( + "encoding/json" + "testing" +) + +func TestCompactionProviderUsageInputCounts(t *testing.T) { + for _, tc := range []struct { + name string + inputProperty string + want int64 + known bool + }{ + {"recorded", `,"inputTokens":37`, 37, true}, + {"replayed", `,"inputTokens":0`, 0, true}, + {"unavailable", ``, 0, false}, + {"input null", `,"inputTokens":null`, 0, false}, + } { + t.Run(tc.name, func(t *testing.T) { + var exchange ParsedHttpExchange + body := `{"compactionUsage":{"interactionId":"compaction-interaction","summary":"summary","responseCount":2` + + tc.inputProperty + `},"requestHeaders":{"x-interaction-type":"conversation-compaction"}}` + if err := json.Unmarshal([]byte(body), &exchange); err != nil { + t.Fatal(err) + } + if exchange.CompactionUsage == nil { + t.Fatal("Expected compaction provider usage") + } + var interactionType string + if err := json.Unmarshal(exchange.RequestHeaders["x-interaction-type"], &interactionType); err != nil || interactionType != "conversation-compaction" { + t.Fatalf("Compaction interaction header = %q, %v", interactionType, err) + } + usage := exchange.CompactionUsage + if usage.InteractionID != "compaction-interaction" || usage.Summary != "summary" || usage.ResponseCount != 2 { + t.Fatalf("Unexpected compaction chain metadata: %+v", usage) + } + var got int64 + known := usage.InputTokens != nil + if known != tc.known { + t.Fatalf("Provider input usage availability = %v, want %v", known, tc.known) + } + if known { + got = *usage.InputTokens + } + if got != tc.want { + t.Fatalf("Provider input tokens = %d, want %d", got, tc.want) + } + }) + } +} diff --git a/go/internal/e2e/tools_e2e_test.go b/go/internal/e2e/tools_e2e_test.go index 75613bebdf..a5109b154b 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + package e2e import ( @@ -18,6 +20,53 @@ func TestToolsE2E(t *testing.T) { client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) + t.Run("string schema apply_patch override binds patch input", func(t *testing.T) { + ctx.ConfigureForTest(t) + const patch = "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch" + type capturedPatch struct { + patch string + arguments any + } + calls := make(chan capturedPatch, 1) + tool := copilot.DefineTool("apply_patch", "Apply a patch", + func(input string, invocation copilot.ToolInvocation) (string, error) { + calls <- capturedPatch{input, invocation.Arguments} + return "HOST_PATCH_HANDLED", nil + }) + tool.OverridesBuiltInTool = true + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{tool}, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + finalMessage := testharness.SubscribeToFinalAssistantMessage(session) + defer finalMessage.Close() + _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use apply_patch to apply the supplied patch."}) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + answer, err := finalMessage.Wait(t.Context()) + if err != nil { + t.Fatalf("Failed to get assistant message: %v", err) + } + select { + case call := <-calls: + if call.patch != patch || call.arguments != patch { + t.Fatalf("Expected scalar patch %q, got %+v", patch, call) + } + default: + t.Fatal("String-schema handler was not invoked") + } + if message, ok := answer.Data.(*copilot.AssistantMessageData); !ok || message.Content != "Host override completed." { + t.Fatalf("Unexpected final response: %v", answer.Data) + } + if _, err := os.Stat(filepath.Join(ctx.WorkDir, "override-marker.txt")); !os.IsNotExist(err) { + t.Fatalf("Native apply_patch must not create the file: %v", err) + } + }) + t.Run("invokes built-in tools", func(t *testing.T) { ctx.ConfigureForTest(t) diff --git a/go/rpc/mcp_list_test.go b/go/rpc/mcp_list_test.go new file mode 100644 index 0000000000..c2e9c47a09 --- /dev/null +++ b/go/rpc/mcp_list_test.go @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "context" + "encoding/json" + "io" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" +) + +var _ interface { + List(context.Context) (*MCPServerList, error) + ListConfigured(context.Context) (*MCPConfiguredServerList, error) +} = (*MCPAPI)(nil) + +func TestMCPListUsesParameterlessWireContract(t *testing.T) { + clientToServerReader, clientToServerWriter := io.Pipe() + serverToClientReader, serverToClientWriter := io.Pipe() + client := jsonrpc2.NewClient(clientToServerWriter, serverToClientReader) + server := jsonrpc2.NewClient(serverToClientWriter, clientToServerReader) + requests := make(chan map[string]any, 2) + server.SetRequestHandler("session.mcp.list", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + server.SetRequestHandler("session.mcp.listConfigured", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request map[string]any + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + requests <- request + return json.RawMessage(`{"servers":[]}`), nil + }) + client.Start() + server.Start() + t.Cleanup(func() { + client.Stop() + server.Stop() + _ = clientToServerWriter.Close() + _ = clientToServerReader.Close() + _ = serverToClientWriter.Close() + _ = serverToClientReader.Close() + }) + + mcp := NewSessionRPC(client, "session-1").MCP + if _, err := mcp.List(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := mcp.ListConfigured(t.Context()); err != nil { + t.Fatal(err) + } + got := make([]map[string]any, 2) + for i := range got { + got[i] = <-requests + } + for _, request := range got { + if request["sessionId"] != "session-1" || len(request) != 1 { + t.Fatalf("request = %#v, want only the bound session ID", request) + } + } +} diff --git a/go/rpc/mcp_prompts_test.go b/go/rpc/mcp_prompts_test.go new file mode 100644 index 0000000000..14cbe69fc9 --- /dev/null +++ b/go/rpc/mcp_prompts_test.go @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" +) + +// Generated serialization supplements the real MCP boundary tests in Node and .NET. +func TestMCPPromptResultsPreserveOpaqueJSON(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", "..", "test", "harness", "mcp-prompt-fixtures.json")) + if err != nil { + t.Fatal(err) + } + var fixtures map[string]json.RawMessage + if err := json.Unmarshal(raw, &fixtures); err != nil { + t.Fatal(err) + } + for _, name := range []string{"firstPage", "secondPage", "richPrompt"} { + t.Run(name, func(t *testing.T) { + var result any = &MCPPromptsListResult{} + if name == "richPrompt" { + result = &MCPPromptsGetResult{} + } + if err := json.Unmarshal(fixtures[name], result); err != nil { + t.Fatal(err) + } + assertPromptJSON(t, result, fixtures[name]) + }) + } +} + +func TestMCPPromptRequestsPreserveOptionalArguments(t *testing.T) { + for _, wire := range []string{ + `{"serverName":"fixture","promptName":"rich"}`, + `{"serverName":"fixture","promptName":"rich","arguments":{}}`, + `{"serverName":"fixture","promptName":"rich","arguments":{"topic":"日本語","style":""}}`, + } { + t.Run(wire, func(t *testing.T) { + var request MCPPromptsGetRequest + if err := json.Unmarshal([]byte(wire), &request); err != nil { + t.Fatal(err) + } + assertPromptJSON(t, request, []byte(wire)) + }) + } + for _, wire := range []string{ + `{"serverName":"fixture"}`, + `{"serverName":"fixture","cursor":"page:2/opaque+cursor="}`, + } { + var request MCPPromptsListRequest + if err := json.Unmarshal([]byte(wire), &request); err != nil { + t.Fatal(err) + } + assertPromptJSON(t, request, []byte(wire)) + } +} + +func assertPromptJSON(t *testing.T, value any, expected []byte) { + t.Helper() + actual, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var got, want any + if err := json.Unmarshal(actual, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(expected, &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("JSON mismatch\n got: %s\nwant: %s", actual, expected) + } +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 2133117a3a..a8a921f58b 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -468,6 +468,37 @@ type AgentReloadResult struct { Agents []AgentInfo `json:"agents"` } +// The models a custom agent asks for, and the models actually available. +// Experimental: AgentsCustomAgentInitialModelDecisionParams is part of an experimental API +// and may change or be removed. +// Internal: AgentsCustomAgentInitialModelDecisionParams is an internal SDK API and is not +// part of the public surface. +type AgentsCustomAgentInitialModelDecisionParams struct { + // The agent's declared `model:` entry, serialized. A single name or an ordered list of + // acceptable names. + AgentModelsJSON string `json:"agentModelsJson"` + // The models available to this session, serialized in the shape the model list carries. + AvailableModelsJSON string `json:"availableModelsJson"` +} + +// The model to switch to, and the warning to show when the agent's preference could not be +// met. +// Experimental: AgentsCustomAgentInitialModelDecisionResult is part of an experimental API +// and may change or be removed. +// Internal: AgentsCustomAgentInitialModelDecisionResult is an internal SDK API and is not +// part of the public surface. +type AgentsCustomAgentInitialModelDecisionResult struct { + // The reasoning effort attached to the selected model preference. Absent when that + // preference does not specify an effort. + ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // The first available model that matches the agent's preferences. Absent when none of the + // requested models is available. + TargetModel *string `json:"targetModel,omitempty"` + // What to tell the user about an unmet preference. Absent when the preference was met. A + // warning with no `targetModel` means the agent's models are all unavailable. + Warning *string `json:"warning,omitempty"` +} + // Optional project paths to include in agent discovery. // Experimental: AgentsDiscoverRequest is part of an experimental API and may change or be // removed. @@ -506,6 +537,101 @@ type AgentSetPromptRequest struct { Prompt string `json:"prompt"` } +// The feature flags to evaluate shipped agents against. +// Experimental: AgentsGetAvailableBuiltinsRequest is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetAvailableBuiltinsRequest is an internal SDK API and is not part of the +// public surface. +type AgentsGetAvailableBuiltinsRequest struct { + // The surface asking, which gates agents that only apply to one client. Omit or pass null + // to apply no client filter. + Context *string `json:"context,omitempty"` + // Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or + // pass null for no flags. + FeatureFlags map[string]any `json:"featureFlags,omitzero"` + // Flag overrides keyed by name. A null entry uses the corresponding base flag; false + // explicitly disables it. Omit or pass null for no overrides. + Overrides map[string]any `json:"overrides,omitzero"` +} + +// The shipped agents available under the requested flags. +// Experimental: AgentsGetAvailableBuiltinsResult is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetAvailableBuiltinsResult is an internal SDK API and is not part of the +// public surface. +type AgentsGetAvailableBuiltinsResult struct { + // Available shipped agents, in the runtime's own order. + // Internal: Agents is part of the SDK's internal API surface and is not intended for + // external use. + Agents []BuiltinAgentSummary `json:"agents"` +} + +// The shipped agent whose definition to load. +// Experimental: AgentsGetBuiltinDefinitionRequest is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetBuiltinDefinitionRequest is an internal SDK API and is not part of the +// public surface. +type AgentsGetBuiltinDefinitionRequest struct { + // The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside + // that list is special-cased in code and has no definition, and is reported as an error + // rather than as an empty definition. + Name string `json:"name"` +} + +// One shipped agent's definition. +// Experimental: AgentsGetBuiltinDefinitionResult is part of an experimental API and may +// change or be removed. +// Internal: AgentsGetBuiltinDefinitionResult is an internal SDK API and is not part of the +// public surface. +type AgentsGetBuiltinDefinitionResult struct { + // The agent's definition, serialized as JSON. It carries the authored keys plus the + // runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather + // than an object because the runtime parses it with the agent schema's tolerant shape, + // which accepts keys this contract does not name. + DefinitionJSON string `json:"definitionJson"` +} + +// The shipped agent whose listing entry to load. +// Experimental: AgentsGetBuiltinListingDefinitionRequest is part of an experimental API and +// may change or be removed. +// Internal: AgentsGetBuiltinListingDefinitionRequest is an internal SDK API and is not part +// of the public surface. +type AgentsGetBuiltinListingDefinitionRequest struct { + // The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the + // agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here + // too, from its in-code definition. + Name string `json:"name"` +} + +// One shipped agent, projected for a listing. +// Experimental: AgentsGetBuiltinListingDefinitionResult is part of an experimental API and +// may change or be removed. +// Internal: AgentsGetBuiltinListingDefinitionResult is an internal SDK API and is not part +// of the public surface. +type AgentsGetBuiltinListingDefinitionResult struct { + // The agent projected as a custom agent, serialized as JSON. It is a string rather than an + // object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying + // definition with the agent schema's tolerant shape, which accepts keys this contract does + // not name. + DefinitionJSON string `json:"definitionJson"` +} + +// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +// Experimental: AgentsGetBuiltinsResult is part of an experimental API and may change or be +// removed. +// Internal: AgentsGetBuiltinsResult is an internal SDK API and is not part of the public +// surface. +type AgentsGetBuiltinsResult struct { + // The subset of `names` a user is allowed to turn off. A shipped agent outside this list is + // always active and a client should not offer a toggle for it. + DisableableNames []string `json:"disableableNames"` + // Every agent name this runtime ships. + Names []string `json:"names"` + // The subset of `names` defined by a shipped YAML definition. The remainder are + // special-cased in code and have no definition to load. + YamlBasedNames []string `json:"yamlBasedNames"` +} + // Optional project paths to include when enumerating agent discovery directories. // Experimental: AgentsGetDiscoveryPathsRequest is part of an experimental API and may // change or be removed. @@ -1004,6 +1130,18 @@ type AuthIdentity struct { Type AuthInfoType `json:"type"` } +// Credential-free identity metadata. +// Experimental: AuthIdentityMetadata is part of an experimental API and may change or be +// removed. +type AuthIdentityMetadata struct { + // Identity host. + Host string `json:"host"` + // User login. + Login string `json:"login"` + // Authentication type. + Type AuthInfoType `json:"type"` +} + // Authentication credentials accepted only at native protocol ingress. Runtime outputs use // credential-free `AuthIdentity` metadata. // Experimental: AuthInfo is part of an experimental API and may change or be removed. @@ -1022,6 +1160,21 @@ func (r RawAuthInfoData) Type() AuthInfoType { return r.Discriminator } +// An interactive account whose model provider owns its credentials. It carries no GitHub +// credential. +// Experimental: AccountAuthInfo is part of an experimental API and may change or be removed. +type AccountAuthInfo struct { + // Host coordinate owned by the account's model provider. + Host string `json:"host"` + // Login identifying the provider-owned account. + Login string `json:"login"` +} + +func (AccountAuthInfo) authInfo() {} +func (AccountAuthInfo) Type() AuthInfoType { + return AuthInfoTypeAccount +} + // Authentication-info input variant for API-key authentication to a non-GitHub LLM // provider, carrying the secret `apiKey` and host. // Experimental: APIKeyAuthInfo is part of an experimental API and may change or be removed. @@ -1181,6 +1334,20 @@ func (UserAuthInfo) Type() AuthInfoType { return AuthInfoTypeUser } +// A credential-free account choice after sign-in. +// Experimental: AuthLoginAccount is part of an experimental API and may change or be +// removed. +type AuthLoginAccount struct { + // Host coordinate owned by the selected account's provider. + Host string `json:"host"` + // Provider kind that owns this account choice. + Kind AccountKind `json:"kind"` + // Human-readable login for the account choice. + Login string `json:"login"` + // Opaque identifier supplied to the next login step to select this account. + SelectionID string `json:"selectionId"` +} + // Advance an in-flight login flow, optionally fulfilling an input-required step. // Experimental: AuthLoginAdvanceRequest is part of an experimental API and may change or be // removed. @@ -1217,15 +1384,18 @@ type AuthLoginCancelRequest struct { FlowID string `json:"flowId"` } -// Terminal result of an interactive login flow. +// Result of an interactive login flow. Pending consent or account selection is not terminal. // Experimental: AuthLoginResultDto is part of an experimental API and may change or be // removed. type AuthLoginResultDto struct { + // Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 + // first. + Accounts []AuthLoginAccount `json:"accounts,omitzero"` // Host that was signed in, when completed. Host *string `json:"host,omitempty"` // Login that was signed in, when completed. Login *string `json:"login,omitempty"` - // Terminal disposition of the login. + // Current disposition of the login, including pending user decisions. Status AuthLoginResultStatus `json:"status"` } @@ -1259,7 +1429,8 @@ func (AuthLoginStepAwaiting) Kind() AuthLoginStepKind { } type AuthLoginStepCompleted struct { - // The terminal login result. + // Login result. When status is needs-plaintext-consent or needs-account-selection, advance + // with the user's decision to continue. Result AuthLoginResultDto `json:"result"` } @@ -1368,6 +1539,9 @@ func (r RawAuthReadValueData) Kind() AuthReadValueKind { type AuthReadValueActiveAccount struct { // The active account, or absent when not logged in. Account *AccountStatus `json:"account,omitempty"` + // Credential-free identity metadata for the active account, including resolved Copilot user + // information when available. + AuthInfo *AuthIdentity `json:"authInfo,omitempty"` } func (AuthReadValueActiveAccount) authReadValue() {} @@ -1528,6 +1702,18 @@ type AutopilotObjectiveState struct { TurnCount int64 `json:"turnCount"` } +// A shipped agent, named and described. +// Experimental: BuiltinAgentSummary is part of an experimental API and may change or be +// removed. +// Internal: BuiltinAgentSummary is an internal SDK API and is not part of the public +// surface. +type BuiltinAgentSummary struct { + // One-line description of what the agent does. + Description string `json:"description"` + // The agent name, as it appears in `getBuiltins`. + Name string `json:"name"` +} + // The running runtime's complete catalog of well-known built-in model IDs, including // supported models and additional IDs with built-in metadata. // Experimental: BuiltInModelCatalog is part of an experimental API and may change or be @@ -3095,6 +3281,10 @@ type ConnectorCapabilities struct { // Whether callers select a host-owned GitHub account through an opaque selection ID rather // than supplying a provider token. OpaqueAccountSelection bool `json:"opaqueAccountSelection"` + // Whether getAccount is supported. Absence means false. + SessionAccountSelection *bool `json:"sessionAccountSelection,omitempty"` + // Whether reconcile accepts forceConnectorName. Absence means false. + TargetedReconcile *bool `json:"targetedReconcile,omitempty"` } // Credential-free Connector catalog entry. @@ -3105,12 +3295,18 @@ type ConnectorCatalogEntry struct { Description *string `json:"description,omitempty"` // Untrusted display label from the service. DisplayName string `json:"displayName"` + // Optional catalog logo. + Logo *string `json:"logo,omitempty"` // Canonical Connector name used by lifecycle methods. Name string `json:"name"` + // Optional catalog release tag. + ReleaseTag *string `json:"releaseTag,omitempty"` // Opaque stable runtime IDs currently projected into the session for this Connector. RuntimeServerIDs []string `json:"runtimeServerIds"` // Current authoritative service connection state. Status ConnectorCatalogStatus `json:"status"` + // Optional catalog tier. + Tier *string `json:"tier,omitempty"` } // Validated Connector catalog snapshot cached by the session. @@ -3215,6 +3411,94 @@ type ConnectorDisconnectResult struct { Status ConnectorStatus `json:"status"` } +// Eligible account. +// Experimental: ConnectorDiscoveryAccount is part of an experimental API and may change or +// be removed. +type ConnectorDiscoveryAccount struct { + // Opaque account ID. + AccountID string `json:"accountId"` + // Account metadata. + AuthInfo ConnectorDiscoveryAuthInfo `json:"authInfo"` +} + +// Eligible accounts. +// Experimental: ConnectorDiscoveryAccountList is part of an experimental API and may change +// or be removed. +type ConnectorDiscoveryAccountList struct { + // Eligible accounts. + Accounts []ConnectorDiscoveryAccount `json:"accounts"` + // Availability. + Availability ConnectorDiscoveryAvailability `json:"availability"` +} + +// Selected account. +// Experimental: ConnectorDiscoveryAccountRequest is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryAccountRequest struct { + // Opaque account ID. + AccountID string `json:"accountId"` +} + +// Account metadata. +// Experimental: ConnectorDiscoveryAuthInfo is part of an experimental API and may change or +// be removed. +type ConnectorDiscoveryAuthInfo struct { + // Host. + Host string `json:"host"` + // Login. + Login string `json:"login"` + // Authentication type. + Type AuthInfoType `json:"type"` +} + +// Feature availability. +// Experimental: ConnectorDiscoveryCapabilities is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCapabilities struct { + // API version. + APIVersion int64 `json:"apiVersion"` + // Availability. + Availability ConnectorDiscoveryAvailability `json:"availability"` + // Whether results are cached. + ConditionalCache bool `json:"conditionalCache"` + // Whether accounts are selected by opaque ID. + OpaqueAccountSelection bool `json:"opaqueAccountSelection"` +} + +// Entry. +// Experimental: ConnectorDiscoveryCatalogEntry is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCatalogEntry struct { + // Description. + Description *string `json:"description,omitempty"` + // Display name. + DisplayName string `json:"displayName"` + // Logo. + Logo *string `json:"logo,omitempty"` + // Name. + Name string `json:"name"` + // Release tag. + ReleaseTag *string `json:"releaseTag,omitempty"` + // Status. + Status ConnectorCatalogStatus `json:"status"` + // Tier. + Tier *string `json:"tier,omitempty"` +} + +// Entries for the selected account. +// Experimental: ConnectorDiscoveryCatalogResult is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryCatalogResult struct { + // Opaque account ID. + AccountID string `json:"accountId"` + // Entries. + Connectors []ConnectorDiscoveryCatalogEntry `json:"connectors"` + // Refresh time in Unix epoch milliseconds. + RefreshedAtMs int64 `json:"refreshedAtMs"` + // Revision. + Revision int64 `json:"revision"` +} + // Requests authoritative Connector-to-MCP reconciliation for the pinned account. // Experimental: ConnectorReconcileRequest is part of an experimental API and may change or // be removed. @@ -3222,6 +3506,8 @@ type ConnectorReconcileRequest struct { // Opaque account selection ID. It must match the account already pinned to the session, if // any. AccountID string `json:"accountId"` + // Optional Connector name to reinitialize. Requires the targetedReconcile capability. + ForceConnectorName *string `json:"forceConnectorName,omitempty"` // When true, refresh the catalog before reconciling. A disabled Connector API performs no // service request. RefreshCatalog *bool `json:"refreshCatalog,omitempty"` @@ -3239,6 +3525,21 @@ type ConnectorRuntimeStatus struct { Status ConnectorMCPStatus `json:"status"` } +// Session account selection. +// Experimental: ConnectorSessionAccount is part of an experimental API and may change or be +// removed. +type ConnectorSessionAccount struct { + // Opaque session-scoped account selection ID. + AccountID string `json:"accountId"` + // Credential-free identity metadata. + AuthInfo AuthIdentityMetadata `json:"authInfo"` +} + +// Session account selection, or null. +// Experimental: ConnectorSessionAccountResult is part of an experimental API and may change +// or be removed. +type ConnectorSessionAccountResult = *ConnectorSessionAccount + // Authoritative session connector state. Account IDs are opaque routing identifiers and // credentials are never included. // Experimental: ConnectorStatus is part of an experimental API and may change or be removed. @@ -3591,6 +3892,10 @@ type CurrentModel struct { // Latest unclaimed Auto preference waiting for a future user turn. Null means the pending // request is returning to provider-default routing. PendingAutoTier *AutoTier `json:"pendingAutoTier,omitempty"` + // Captured base model to restore when leaving plan mode. Omitted outside plan mode or when + // no plan override has captured a base model. Persistent agent model requirements apply to + // this model rather than the temporary plan model. + PlanBaseModelID *string `json:"planBaseModelId,omitempty"` // Reasoning effort level currently applied to the active model, when one is set. Reads // `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the // two values are reported as a snapshot. @@ -3617,6 +3922,33 @@ type CurrentToolMetadata struct { NamespacedName *string `json:"namespacedName,omitempty"` } +// Result of one customization reload component. +// Experimental: CustomizationReloadOutcome is part of an experimental API and may change or +// be removed. +type CustomizationReloadOutcome struct { + // Reason for a skipped component or description of a failure, when available + Detail *string `json:"detail,omitempty"` + // Whether the component reloaded, was skipped, or failed + Status CustomizationReloadStatus `json:"status"` + // Component whose reload was attempted or skipped + Subsystem CustomizationReloadSubsystem `json:"subsystem"` +} + +// Results of reloading discovered session customizations. Inspect outcomes for reloaded, +// skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the +// model-facing prompt and tools apply on the next turn. +// Experimental: CustomizationsReloadResult is part of an experimental API and may change or +// be removed. +type CustomizationsReloadResult struct { + // Errors from any component that could not be refreshed + Errors []string `json:"errors"` + // Outcome of each component in reload order; a skipped component was not configured or + // loaded + Outcomes []CustomizationReloadOutcome `json:"outcomes"` + // Warnings from skill discovery + Warnings []string `json:"warnings"` +} + // A file included in the session debug bundle. // Experimental: DebugCollectLogsCollectedEntry is part of an experimental API and may // change or be removed. @@ -3958,6 +4290,44 @@ type DiscoveredMCPServer struct { Type *DiscoveredMCPServerType `json:"type,omitempty"` } +// A model offered for agent conversations. Missing capability metadata does not disqualify +// a candidate. Models known to be incompatible, such as embedding-only models, are excluded +// by the adapter. +// Experimental: DiscoveredModel is part of an experimental API and may change or be removed. +type DiscoveredModel struct { + // Provider-reported model capabilities. Omitted capability fields are unknown; explicit + // false values are preserved. + Capabilities ModelCapabilities `json:"capabilities"` + // Provider-reported model artifact details. + Details ModelArtifactDetails `json:"details"` + // Provider-reported artifact digest. + Digest *string `json:"digest,omitempty"` + // Provider-native model identifier. + ID string `json:"id"` + // Provider-reported last-modified timestamp. + ModifiedAt *time.Time `json:"modifiedAt,omitempty"` + // Provider-reported display name. + Name *string `json:"name,omitempty"` + // Attribution for the adapter that produced this model row. + Provenance ModelProviderProvenance `json:"provenance"` + // Provider-reported artifact size in bytes. + SizeBytes *int64 `json:"sizeBytes,omitempty"` + // Non-fatal warnings encountered while enriching this model. + Warnings []ModelProviderWarning `json:"warnings"` +} + +// Models offered for agent conversations by one provider instance. Adapters exclude +// known-incompatible models, but retain candidates with unknown capabilities. Listing does +// not guarantee compatibility. +// Experimental: DiscoveredModelList is part of an experimental API and may change or be +// removed. +type DiscoveredModelList struct { + // Provider-native models in provider order. + Models []DiscoveredModel `json:"models"` + // Typed operation outcome. + Outcome ModelProviderOperationOutcome `json:"outcome"` +} + // Slash-prefixed command string to enqueue for FIFO processing. // Experimental: EnqueueCommandParams is part of an experimental API and may change or be // removed. @@ -4001,66 +4371,6 @@ func (UnsupportedEnqueueCommandResult) Queued() bool { return false } -// OneAuth token request supplied by a trusted host application. -// Experimental: EntraTokenAcquireRequest is part of an experimental API and may change or -// be removed. -type EntraTokenAcquireRequest struct { - // Previously rejected token that OneAuth must bypass during renewal. - AccessTokenToRenew *string `json:"accessTokenToRenew,omitempty"` - // Public client application id. - ClientID string `json:"clientId"` - // Whether the broker may show interaction. - Interaction EntraTokenInteraction `json:"interaction"` - // Broker redirect URI registered for the client. Required: the OneAuth broker validates a - // non-empty, registered redirect URI for the public client (MSAL broker registration), so - // this is not a browser-flow vestige and cannot be omitted. - RedirectURI string `json:"redirectUri"` - // Exact delegated scopes to request. - Scopes []string `json:"scopes"` - // Tenant id or tenant selector, such as common or organizations. - TenantID string `json:"tenantId"` -} - -// Result of a OneAuth token acquisition. -// Experimental: EntraTokenAcquireResult is part of an experimental API and may change or be -// removed. -type EntraTokenAcquireResult interface { - entraTokenAcquireResult() - Status() EntraTokenAcquireResultStatus -} - -type RawEntraTokenAcquireResultData struct { - Discriminator EntraTokenAcquireResultStatus - Raw json.RawMessage -} - -func (RawEntraTokenAcquireResultData) entraTokenAcquireResult() {} -func (r RawEntraTokenAcquireResultData) Status() EntraTokenAcquireResultStatus { - return r.Discriminator -} - -type EntraTokenAcquireResultInteractionRequired struct { -} - -func (EntraTokenAcquireResultInteractionRequired) entraTokenAcquireResult() {} -func (EntraTokenAcquireResultInteractionRequired) Status() EntraTokenAcquireResultStatus { - return EntraTokenAcquireResultStatusInteractionRequired -} - -type EntraTokenAcquireResultOk struct { - // Opaque access token. - AccessToken string `json:"accessToken"` - // Opaque OneAuth account id, when supplied by the broker. - AccountID *string `json:"accountId,omitempty"` - // Expiry as milliseconds since Unix epoch, when supplied by OneAuth. - ExpiresOnTimestamp *float64 `json:"expiresOnTimestamp,omitempty"` -} - -func (EntraTokenAcquireResultOk) entraTokenAcquireResult() {} -func (EntraTokenAcquireResultOk) Status() EntraTokenAcquireResultStatus { - return EntraTokenAcquireResultStatusOk -} - // Hosting capabilities and session capacity advertised by an environment. // Experimental: EnvironmentCapabilities is part of an experimental API and may change or be // removed. @@ -4662,6 +4972,26 @@ type FolderTrustCheckResult struct { Trusted bool `json:"trusted"` } +// The remote the checked-out branch tracks. +// Experimental: GitCurrentBranchRemoteResult is part of an experimental API and may change +// or be removed. +// Internal: GitCurrentBranchRemoteResult is an internal SDK API and is not part of the +// public surface. +type GitCurrentBranchRemoteResult struct { + // Name of the tracked remote. Reports `origin` whenever the working tree has no tracking + // configuration to read, including on a detached HEAD, so this is never null and never + // empty. + Remote string `json:"remote"` +} + +// Working-tree path a git query applies to. +// Experimental: GitCwdRequest is part of an experimental API and may change or be removed. +// Internal: GitCwdRequest is an internal SDK API and is not part of the public surface. +type GitCwdRequest struct { + // Absolute path to a directory inside the git working tree to query. + Cwd string `json:"cwd"` +} + // Safe discovery information. Host-side relay bootstrap credentials are never included. // Experimental: GitHubEnvironment is part of an experimental API and may change or be // removed. @@ -4688,6 +5018,91 @@ type GitHubEnvironment struct { Status string `json:"status"` } +// A GitHub login the authenticated user may act as: their own account, or an organization +// they belong to. +// Experimental: GitHubOwnerOption is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnerOption is an internal SDK API and is not part of the public surface. +type GitHubOwnerOption struct { + // The owner's GitHub login. + Login string `json:"login"` + // Which kind of owner this is. The authenticated user's own account is always reported as + // `user`. + Type string `json:"type"` +} + +// The owner listing to abandon. +// Experimental: GitHubOwnersCancelRequest is part of an experimental API and may change or +// be removed. +// Internal: GitHubOwnersCancelRequest is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersCancelRequest struct { + // Request id the listing was started with. + RequestID int64 `json:"requestId"` +} + +// Whether the id named a running owner listing. +// Experimental: GitHubOwnersCancelResult is part of an experimental API and may change or +// be removed. +// Internal: GitHubOwnersCancelResult is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersCancelResult struct { + // True when a listing with the id was running and the cancel stopped it. False when the id + // was never registered, was registered but unused, was released after being abandoned, or + // its listing had ended. An unused id is released and cannot start a later listing. + Canceled bool `json:"canceled"` +} + +// Credential to list owners under, and the request id that makes the listing cancellable. +// Experimental: GitHubOwnersListRequest is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnersListRequest is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersListRequest struct { + // The credential the listing runs under, carried opaquely because its shape is the host's + // own and the runtime only resolves a token and a GitHub host from it. No credential + // travels: this selects one the runtime already holds. + AuthInfo any `json:"authInfo"` + // Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled + // before use, released after being abandoned, or already used is refused rather than + // silently running uncancellable. + RequestID int64 `json:"requestId"` +} + +// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except +// that `throwError` reports a failure the caller is expected to raise rather than render. +// Experimental: GitHubOwnersListResult is part of an experimental API and may change or be +// removed. +// Internal: GitHubOwnersListResult is an internal SDK API and is not part of the public +// surface. +type GitHubOwnersListResult struct { + // Why no owners could be listed, phrased for a user. Present when the listing failed in a + // way the caller should render rather than raise. + Message *string `json:"message,omitempty"` + // The owners, on success: the authenticated user first, then the organizations they belong + // to. + // Internal: Owners is part of the SDK's internal API surface and is not intended for + // external use. + Owners []GitHubOwnerOption `json:"owners,omitzero"` + // A malformed request or an unreadable credential, which the caller raises instead of + // rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, + // which the caller renders. + ThrowError *string `json:"throwError,omitempty"` + // A line the caller should log. Present only alongside `message`, and only for failures + // worth recording. + Warning *string `json:"warning,omitempty"` +} + +// A freshly registered request id. Registering it before the listing starts is what lets a +// cancel that races the request still find the owner listing slot. The id serves one +// listing only. Long-abandoned unused ids can be released by later allocations. +// Experimental: GitHubOwnersRequestIDResult is part of an experimental API and may change +// or be removed. +type GitHubOwnersRequestIDResult struct { + // Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + RequestID int64 `json:"requestId"` +} + // Pointer to a GitHub repository. // Experimental: GitHubRepoRef is part of an experimental API and may change or be removed. type GitHubRepoRef struct { @@ -4699,6 +5114,39 @@ type GitHubRepoRef struct { Owner string `json:"owner"` } +// Working-tree path whose owning GitHub repository should be resolved. +// Experimental: GitHubRepositoryAtPathRequest is part of an experimental API and may change +// or be removed. +// Internal: GitHubRepositoryAtPathRequest is an internal SDK API and is not part of the +// public surface. +type GitHubRepositoryAtPathRequest struct { + // Absolute path to a directory inside the git working tree to resolve. + Path string `json:"path"` +} + +// The GitHub repository that owns the requested path, when the selected remote (`origin`, +// else the first) is on a GitHub host. +// Experimental: GitHubRepositoryAtPathResult is part of an experimental API and may change +// or be removed. +// Internal: GitHubRepositoryAtPathResult is an internal SDK API and is not part of the +// public surface. +type GitHubRepositoryAtPathResult struct { + // Resolved repository identity, or null when the selected remote resolves to no GitHub host. + Repository *GitHubRepositoryIdentity `json:"repository,omitempty"` +} + +// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. +// Experimental: GitHubRepositoryIdentity is part of an experimental API and may change or +// be removed. +type GitHubRepositoryIdentity struct { + // Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + Host string `json:"host"` + // Repository name, without the owner prefix or the `.git` suffix. + Name string `json:"name"` + // Repository owner login (user or organization). + Owner string `json:"owner"` +} + // Client environment metadata describing the process that produced a telemetry event. // Experimental: GitHubTelemetryClientInfo is part of an experimental API and may change or // be removed. @@ -4835,6 +5283,142 @@ func (GitHubTokenAcquireResultToken) Kind() GitHubTokenAcquireResultKind { return GitHubTokenAcquireResultKindToken } +// A GitHub repository one of a working tree's remotes points at. +// Experimental: GitRemoteRepository is part of an experimental API and may change or be +// removed. +// Internal: GitRemoteRepository is an internal SDK API and is not part of the public +// surface. +type GitRemoteRepository struct { + // GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise + // remote. + Host string `json:"host"` + // Repository name, without the owner. + Name string `json:"name"` + // Account or organization owning the repository. + Owner string `json:"owner"` + // Name of the first remote that produced this distinct repository entry, such as `origin` + // or `upstream`. + RemoteName string `json:"remoteName"` +} + +// Git working tree whose GitHub remotes should be listed. +// Experimental: GitReposFromRemotesRequest is part of an experimental API and may change or +// be removed. +// Internal: GitReposFromRemotesRequest is an internal SDK API and is not part of the public +// surface. +type GitReposFromRemotesRequest struct { + // Absolute path to the root of the git working tree. + GitRoot string `json:"gitRoot"` +} + +// The GitHub repositories a working tree's remotes point at. +// Experimental: GitReposFromRemotesResult is part of an experimental API and may change or +// be removed. +// Internal: GitReposFromRemotesResult is an internal SDK API and is not part of the public +// surface. +type GitReposFromRemotesResult struct { + // One entry per distinct GitHub repository, in the order git reports the first remote for + // each repository. Empty when no remote points at a GitHub host, which a caller should read + // as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + // Internal: Repositories is part of the SDK's internal API surface and is not intended for + // external use. + Repositories []GitRemoteRepository `json:"repositories"` +} + +// Selects the configuration directory whose machine-wide state to read. +// Experimental: GlobalStateLoadForConfigDirRequest is part of an experimental API and may +// change or be removed. +// Internal: GlobalStateLoadForConfigDirRequest is an internal SDK API and is not part of +// the public surface. +type GlobalStateLoadForConfigDirRequest struct { + // Copilot configuration directory to read the state document from, taking precedence over + // the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + // read the directory the server resolved for itself. + ConfigDir *string `json:"configDir,omitempty"` +} + +// The host's machine-wide state. Every field is optional because a fresh install has +// recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Experimental: GlobalStateLoadResult is part of an experimental API and may change or be +// removed. +// Internal: GlobalStateLoadResult is an internal SDK API and is not part of the public +// surface. +type GlobalStateLoadResult struct { + // Whether the user has answered the prompt suggesting they install the desktop app. + AppInstallNudgeResponded *bool `json:"appInstallNudgeResponded,omitempty"` + // Whether the app tip has been shown. + AppTipShown *bool `json:"appTipShown,omitempty"` + // Terminals the user has already been asked to set up, so the host does not ask twice. + AskedSetupTerminals []string `json:"askedSetupTerminals,omitzero"` + // When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the + // once-per-day cap for non-staff users across restarts. + AutoFeedbackLastPromptedAt *string `json:"autoFeedbackLastPromptedAt,omitempty"` + // When the host first ran on this machine. + FirstLaunchAt *string `json:"firstLaunchAt,omitempty"` + // Plugins installed on this machine. + InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` + // Account used for the most recent sign-in. + // Internal: LastLoggedInUser is part of the SDK's internal API surface and is not intended + // for external use. + LastLoggedInUser *LoggedInUser `json:"lastLoggedInUser,omitempty"` + // Every account the host has signed in to on this machine. + // Internal: LoggedInUsers is part of the SDK's internal API surface and is not intended for + // external use. + LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitzero"` + // Whether the one-off cleanup of stored reasoning summaries has run. + ReasoningSummariesCleanupDone *bool `json:"reasoningSummariesCleanupDone,omitempty"` + // Models the user selected recently, most recent first. + RecentModelIDs []string `json:"recentModelIds,omitzero"` + // Whether the user declined to trust the sandbox credential proxy CA. + SandboxCredentialProxyCaDeclined *bool `json:"sandboxCredentialProxyCaDeclined,omitempty"` + // Whether the sandbox onboarding has been shown. + SandboxOnboardingShown *bool `json:"sandboxOnboardingShown,omitempty"` + // Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only + // behavior. + Staff *bool `json:"staff,omitempty"` + // Whether the user was recognized as GitHub staff. + StaffGitHub *bool `json:"staffGithub,omitempty"` + // When the staff-only log level migration last ran. + StaffLogLevelMigrationAt *string `json:"staffLogLevelMigrationAt,omitempty"` + // Whether the user was recognized as Microsoft staff. + StaffMicrosoft *bool `json:"staffMicrosoft,omitempty"` + // When the staff-only model reset last ran. + StaffModelResetAt *string `json:"staffModelResetAt,omitempty"` + // When the staff-only update channel migration last ran. + StaffUpdateChannelMigrationAt *string `json:"staffUpdateChannelMigrationAt,omitempty"` + // Folders where the user declined the init prompt, so it stays hidden there. + SuppressInitFolders []string `json:"suppressInitFolders,omitzero"` + // Folders the user has marked as trusted. + TrustedFolders []string `json:"trustedFolders,omitzero"` +} + +// A single top-level key to record in the host's machine-wide state. The write replaces +// only that key and leaves the rest of the document untouched, so two writers recording +// different one-off flags do not overwrite each other. The stored credential keys cannot be +// written through this method. +// Experimental: GlobalStateWriteKeyRequest is part of an experimental API and may change or +// be removed. +// Internal: GlobalStateWriteKeyRequest is an internal SDK API and is not part of the public +// surface. +type GlobalStateWriteKeyRequest struct { + // Copilot configuration directory to write the state document in, taking precedence over + // the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to + // write the directory the server resolved for itself. Mirrors + // `globalState.loadForConfigDir`, so a caller can read and write the same directory. + ConfigDir *string `json:"configDir,omitempty"` + // Top-level key to write, named as it appears in the result of `globalState.load`. It must + // be one of the writable keys that `globalState.writeKey` lists. + Key string `json:"key"` + // Value to store for the key. Omit it, or pass null, to remove the key instead. + Value any `json:"value,omitempty"` +} + +// Experimental: GlobalStateWriteKeyResult is part of an experimental API and may change or +// be removed. +type GlobalStateWriteKeyResult struct { +} + // 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 @@ -6035,6 +6619,22 @@ type LocalSessionMetadataValue struct { Summary *string `json:"summary,omitempty"` } +// An account the host has signed in to, identified by the server it lives on and the login +// it uses there. The same person can appear more than once when they use both github.com +// and an Enterprise server. +// Experimental: LoggedInUser is part of an experimental API and may change or be removed. +// Internal: LoggedInUser is an internal SDK API and is not part of the public surface. +type LoggedInUser struct { + // Source account this account was derived from, when one was recorded. + DerivedFrom *string `json:"derivedFrom,omitempty"` + // Host the account belongs to, such as `github.com` or an Enterprise server. + Host string `json:"host"` + // Account kind, when the host recorded one. Consumers must tolerate new strings. + Kind *string `json:"kind,omitempty"` + // Account login on that host. + Login string `json:"login"` +} + // Message text, optional severity level, persistence flag, optional follow-up URL, and // optional tip. // Experimental: LogRequest is part of an experimental API and may change or be removed. @@ -6739,6 +7339,48 @@ type MCPConfigUpdateRequest struct { type MCPConfigUpdateResult struct { } +// Effective MCP configuration entry. Configuration enablement is distinct from the optional +// live observation. +// Experimental: MCPConfiguredServer is part of an experimental API and may change or be +// removed. +type MCPConfiguredServer struct { + // Human-readable display name supplied by configuration. + DisplayName *string `json:"displayName,omitempty"` + // Whether this configured server is enabled after session configuration and policy + // filtering. + Enabled bool `json:"enabled"` + // Observed state from an already materialized matching server. Omitted when no live graph + // has this configured server; it never determines configuration enablement. + Live *MCPConfiguredServerState `json:"live,omitempty"` + // Server name (config key) + Name string `json:"name"` + // Configuration provenance: user, workspace, plugin, builtin, or managed. + Source *MCPServerSource `json:"source,omitempty"` + // Plugin name that provided this server, when source is plugin. + SourcePlugin *string `json:"sourcePlugin,omitempty"` + // Plugin version that provided this server, when source is plugin. + SourcePluginVersion *string `json:"sourcePluginVersion,omitempty"` +} + +// Effective MCP configuration with optional live observations from matching already +// materialized servers. +// Experimental: MCPConfiguredServerList is part of an experimental API and may change or be +// removed. +type MCPConfiguredServerList struct { + // Effective configured MCP servers. + Servers []MCPConfiguredServer `json:"servers"` +} + +// Observational state for a matching already materialized MCP server. +// Experimental: MCPConfiguredServerState is part of an experimental API and may change or +// be removed. +type MCPConfiguredServerState struct { + // Observed connection error, when the materialized server failed. + Error *string `json:"error,omitempty"` + // Observed connection status. This is not a configuration or readiness guarantee. + Status MCPServerStatus `json:"status"` +} + // Credential-free authentication identity used to configure GitHub MCP. // Experimental: MCPConfigureGitHubRequest is part of an experimental API and may change or // be removed. @@ -7597,6 +8239,18 @@ type MCPOauthCancelLoginResult struct { Cancelled bool `json:"cancelled"` } +// Host-delivered callback for a runtime-managed MCP OAuth login. +// Experimental: MCPOauthCompleteRequest is part of an experimental API and may change or be +// removed. +type MCPOauthCompleteRequest struct { + // Opaque identifier returned by session.mcp.oauth.login for the pending external callback. + AuthorizationID string `json:"authorizationId"` + // Full externally visible HTTPS callback URL received by the host, including the + // authorization response query parameters. Applications behind a reverse proxy must + // reconstruct the public URL rather than passing an internal proxy URL. + CallbackURL string `json:"callbackUrl"` +} + // Pending MCP OAuth request ID and host-provided token or cancellation response. // Experimental: MCPOauthHandlePendingRequest is part of an experimental API and may change // or be removed. @@ -7617,7 +8271,7 @@ type MCPOauthHandlePendingResult struct { } // Remote MCP server name and optional overrides controlling reauthentication, OAuth client -// display name, callback success-page copy, and static OAuth client selection. +// display name, callback handling, and static OAuth client selection. // Experimental: MCPOauthLoginRequest is part of an experimental API and may change or be // removed. type MCPOauthLoginRequest struct { @@ -7654,6 +8308,12 @@ type MCPOauthLoginRequest struct { // runtime treats it as confidential and uses the per-login clientSecret if provided, // otherwise retrieving the client secret from the MCP OAuth secret store. PublicClient *bool `json:"publicClient,omitempty"` + // Optional externally visible HTTPS redirect URI for a host-managed callback endpoint. When + // supplied, the runtime still owns discovery, PKCE, token exchange, persistence, and + // reconnect, but does not bind a loopback listener or terminate HTTPS. The URI must not + // contain query parameters or a fragment and must be registered for the selected CIMD, DCR, + // or static OAuth client. + RedirectURI *string `json:"redirectUri,omitempty"` // Name of the remote MCP server to authenticate ServerName string `json:"serverName"` } @@ -7663,11 +8323,16 @@ type MCPOauthLoginRequest struct { // Experimental: MCPOauthLoginResult is part of an experimental API and may change or be // removed. type MCPOauthLoginResult struct { + // Opaque authorization identifier returned only for a host-managed redirect URI. The + // runtime also sends it as the OAuth state value, so the callback endpoint can read state + // and pass it with the full callback URL to session.mcp.oauth.complete. + AuthorizationID *string `json:"authorizationId,omitempty"` // URL the caller should open in a browser to complete OAuth. Omitted when cached tokens // were still valid and no browser interaction was needed — the server is already - // reconnected in that case. When present, the runtime starts the callback listener before - // returning and continues the flow in the background; completion is signaled via - // session.mcp_server_status_changed. + // reconnected in that case. For the default loopback flow, the runtime starts its listener + // before returning. With redirectUri, the host receives the callback and completes it + // through session.mcp.oauth.complete. The runtime continues the flow in the background and + // signals completion via session.mcp_server_status_changed. AuthorizationURL *string `json:"authorizationUrl,omitempty"` // Runtime-issued owned flow identity; never a server name or installation operation ID. LoginID *string `json:"loginId,omitempty"` @@ -8320,6 +8985,122 @@ type MCPPrepareInstallRequest struct { Source MCPServerCardReference `json:"source"` } +// An MCP prompt descriptor. Server-provided non-standard fields are exposed under +// `additionalProperties`. +// Experimental: MCPPrompt is part of an experimental API and may change or be removed. +type MCPPrompt struct { + // Server-provided non-standard descriptor fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Arguments accepted by the prompt + Arguments []MCPPromptArgument `json:"arguments,omitzero"` + // Description of what this prompt provides + Description *string `json:"description,omitempty"` + // Icons associated with this prompt + Icons []MCPPromptIcon `json:"icons,omitzero"` + // Prompt-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // The programmatic name of the prompt + Name string `json:"name"` + // Human-readable display title + Title *string `json:"title,omitempty"` +} + +// An argument accepted by an MCP prompt. +// Experimental: MCPPromptArgument is part of an experimental API and may change or be +// removed. +type MCPPromptArgument struct { + // Server-provided non-standard argument fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Description of the argument + Description *string `json:"description,omitempty"` + // Argument-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // Name of the argument + Name string `json:"name"` + // Whether the argument is required; omission is distinct from false + Required *bool `json:"required,omitempty"` +} + +// An MCP prompt icon with standard size hints and preserved non-standard fields. +// Experimental: MCPPromptIcon is part of an experimental API and may change or be removed. +type MCPPromptIcon struct { + // Server-provided non-standard icon fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Icon MIME type, when known + MIMEType *string `json:"mimeType,omitempty"` + // Icon sizes, such as `48x48` or `any` + Sizes []string `json:"sizes,omitzero"` + // Icon URI + Src string `json:"src"` + // Theme hint for this icon + Theme *string `json:"theme,omitempty"` +} + +// An MCP prompt message with opaque JSON content preserved without flattening or +// content-type filtering. +// Experimental: MCPPromptMessage is part of an experimental API and may change or be +// removed. +type MCPPromptMessage struct { + // Server-provided non-standard message fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // The original MCP content block, including nested metadata and unfamiliar content types + Content any `json:"content"` + // Message-level metadata + Meta map[string]any `json:"_meta,omitzero"` + // The role of the message sender + Role MCPPromptRole `json:"role"` +} + +// MCP server, prompt name, and optional string-valued arguments. +// Experimental: MCPPromptsGetRequest is part of an experimental API and may change or be +// removed. +type MCPPromptsGetRequest struct { + // String-valued arguments to pass to the prompt + Arguments map[string]string `json:"arguments,omitzero"` + // The programmatic name of the prompt + PromptName string `json:"promptName"` + // Name of the MCP server hosting the prompt + ServerName string `json:"serverName"` +} + +// Prompt messages returned by the MCP server without sending them to the model. +// Experimental: MCPPromptsGetResult is part of an experimental API and may change or be +// removed. +type MCPPromptsGetResult struct { + // Server-provided non-standard result fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // Description of the prompt + Description *string `json:"description,omitempty"` + // Ordered prompt messages + Messages []MCPPromptMessage `json:"messages"` + // MCP result metadata + Meta map[string]any `json:"_meta,omitzero"` +} + +// MCP server whose prompts to enumerate. +// Experimental: MCPPromptsListRequest is part of an experimental API and may change or be +// removed. +type MCPPromptsListRequest struct { + // Opaque MCP pagination cursor from a prior `nextCursor` value + Cursor *string `json:"cursor,omitempty"` + // Name of the MCP server whose prompts to enumerate + ServerName string `json:"serverName"` +} + +// One page of prompts advertised by the named MCP server. +// Experimental: MCPPromptsListResult is part of an experimental API and may change or be +// removed. +type MCPPromptsListResult struct { + // Server-provided non-standard result fields + AdditionalProperties map[string]any `json:"additionalProperties,omitzero"` + // MCP result metadata + Meta map[string]any `json:"_meta,omitzero"` + // Opaque cursor for the next page, if the server has more prompts + NextCursor *string `json:"nextCursor,omitempty"` + // Prompts advertised by the server + Prompts []MCPPrompt `json:"prompts"` +} + // Registration parameters for an external MCP client. // Experimental: MCPRegisterExternalClientRequest is part of an experimental API and may // change or be removed. @@ -8775,6 +9556,9 @@ type MCPServer struct { // Connection status: connected, failed, needs-auth, pending, disabled, stopped, or // not_configured Status MCPServerStatus `json:"status"` + // Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for + // local and in-memory servers. + URL *string `json:"url,omitempty"` } // Set to `true` to use defaults, or provide an object with additional auth or OIDC settings. @@ -9159,9 +9943,11 @@ type MetadataContextHeaviestMessagesResult struct { // Experimental: MetadataContextInfoRequest is part of an experimental API and may change or // be removed. type MetadataContextInfoRequest struct { - // Maximum output tokens allowed by the target model. Pass 0 if unknown. + // Requested output allowance to reserve against the combined context ceiling. Pass 0 to + // resolve the session's request cap, falling back to the model's advertised output limit. OutputTokenLimit int64 `json:"outputTokenLimit"` - // Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + // Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from + // the session. PromptTokenLimit int64 `json:"promptTokenLimit"` // Model identifier used for tokenization. Omit to use the session default. Used both for // token counting and to compute display values. @@ -9351,6 +10137,11 @@ type Model struct { SupportedContextTiers []string `json:"supportedContextTiers,omitzero"` // Supported reasoning effort levels (only present if model supports reasoning effort) SupportedReasoningEfforts []string `json:"supportedReasoningEfforts,omitzero"` + // Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". + // Open vocabulary, passed through unchanged. It can name the vendor that serves the model + // instead of the one that built it, or a label that is not a vendor, such as + // "Experimental". Absent when the Copilot API reports no vendor. + Vendor *string `json:"vendor,omitempty"` // 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. @@ -9398,6 +10189,26 @@ type ModelApplyStartupOverlayRequest struct { ServerManagedModel *string `json:"serverManagedModel,omitempty"` } +// Provider-reported model artifact metadata. +// Experimental: ModelArtifactDetails is part of an experimental API and may change or be +// removed. +type ModelArtifactDetails struct { + // Provider-reported model architecture. + Architecture *string `json:"architecture,omitempty"` + // Provider-reported model families. + Families []string `json:"families,omitzero"` + // Primary model family. + Family *string `json:"family,omitempty"` + // Artifact format, such as `gguf`. + Format *string `json:"format,omitempty"` + // Provider-reported parameter count label. + ParameterSize *string `json:"parameterSize,omitempty"` + // Provider-reported quantization label. + Quantization *string `json:"quantization,omitempty"` + // Provider-reported tokenizer. + Tokenizer *string `json:"tokenizer,omitempty"` +} + // Billing information // Experimental: ModelBilling is part of an experimental API and may change or be removed. type ModelBilling struct { @@ -9593,6 +10404,9 @@ type ModelCapabilitiesSupports struct { AdaptiveThinking *AdaptiveThinkingSupport `json:"adaptive_thinking,omitempty"` // Whether this model supports reasoning effort configuration ReasoningEffort *bool `json:"reasoningEffort,omitempty"` + // Whether the model supports provider-native thinking. Independent of configurable + // reasoning effort; omission means unknown. + Thinking *bool `json:"thinking,omitempty"` // Whether this model supports canonical tool calling ToolCalls *bool `json:"toolCalls,omitempty"` // Whether this model supports vision/image input @@ -9656,6 +10470,106 @@ type ModelPolicy struct { Terms *string `json:"terms,omitempty"` } +// Normalized model-provider adapter definitions available to the session, not discovered +// instances. +// Experimental: ModelProviderAdapterCatalog is part of an experimental API and may change +// or be removed. +type ModelProviderAdapterCatalog struct { + // Available provider adapters ordered by adapterId. + Providers []ModelProviderAdapterDescriptor `json:"providers"` +} + +// A normalized model-provider adapter in the session's effective catalog. +// Experimental: ModelProviderAdapterDescriptor is part of an experimental API and may +// change or be removed. +type ModelProviderAdapterDescriptor struct { + // Stable opaque identity for routing to this adapter. Unique in the effective catalog, + // independent of live registration generations. + AdapterID string `json:"adapterId"` + // Adapter-declared policy for passive and automatic discovery. + AutomaticDiscovery ModelProviderAutomaticDiscoveryPolicy `json:"automaticDiscovery"` + // Human-readable provider name. + DisplayName string `json:"displayName"` + // Operations supported by this provider adapter. + Operations []ModelProviderAdapterOperationDescriptor `json:"operations"` + // Attribution for the adapter itself. + Provenance ModelProviderAttribution `json:"provenance"` + // Descriptive provider family, such as `ollama`. Different adapters may have the same + // family; use adapterId for routing. + ProviderKind string `json:"providerKind"` +} + +// An operation supported by a model-provider adapter. +// Experimental: ModelProviderAdapterOperationDescriptor is part of an experimental API and +// may change or be removed. +type ModelProviderAdapterOperationDescriptor struct { + // Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported + // on discover. No external references are resolved. Omitted or null input selects defaults + // when requiresInput is false. Without a schema, the adapter validates supplied input. + InputSchema any `json:"inputSchema,omitempty"` + // Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and + // duplicate declarations are rejected. + Name string `json:"name"` +} + +// Contributor attribution, independent of routing identity and authorization. +// Experimental: ModelProviderAttribution is part of an experimental API and may change or +// be removed. +type ModelProviderAttribution struct { + // Human-readable contributor name, not the adapter display name. + OwnerDisplayName *string `json:"ownerDisplayName,omitempty"` + // Stable contributor identifier. Required and nonblank for extension and custom sources; + // optional for built-in and configured sources. Does not grant authority. + OwnerID *string `json:"ownerId,omitempty"` + // Kind of component that supplied the adapter. Attribution does not confer authority. + Source ModelProviderProvenanceSource `json:"source"` +} + +// Adapter-declared policy that tells clients whether discovery may run automatically. +// Experimental: ModelProviderAutomaticDiscoveryPolicy is part of an experimental API and +// may change or be removed. +type ModelProviderAutomaticDiscoveryPolicy struct { + // Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + Mode ModelProviderAutomaticDiscoveryMode `json:"mode"` + // Maximum network scope used by this adapter during discovery. + NetworkScope ModelProviderDiscoveryNetworkScope `json:"networkScope"` + // True when discovery requires non-null caller input. Omission or null is rejected before + // adapter execution. When false, omitted or null input selects adapter defaults without + // schema validation. + RequiresInput bool `json:"requiresInput"` + // True when the adapter must be enabled by a trusted owner, such as a trusted extension, + // before automatic discovery may run. + RequiresTrust bool `json:"requiresTrust"` +} + +// Provider configuration prepared from a discovered model. Preparing a plan changes +// nothing: it neither registers the model with the session nor writes durable +// configuration. To apply it, pass `provider` and `model` to `session.provider.add`, +// omitting whichever the dispositions report as already configured. +// Experimental: ModelProviderConfigurationPlan is part of an experimental API and may +// change or be removed. +type ModelProviderConfigurationPlan struct { + // Model definition prepared from the discovered model. Capability fields the provider did + // not report stay omitted rather than being asserted false. + Model ProviderModelConfig `json:"model"` + // Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is + // already registered and the caller can select it without adding anything. + ModelDisposition ModelProviderConfigurationDisposition `json:"modelDisposition"` + // Provider connection prepared from the instance's inference metadata. Carries no + // credential; supply one if the endpoint requires it. + Provider NamedProviderConfig `json:"provider"` + // Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider + // with the same endpoint is already registered and `provider` restates it under its + // existing name; adding it again is rejected as a duplicate. + ProviderDisposition ModelProviderConfigurationDisposition `json:"providerDisposition"` + // Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is + // applied. + SelectionID string `json:"selectionId"` + // Non-fatal warnings carried over from the discovered model, such as capabilities the + // provider did not report. + Warnings []ModelProviderWarning `json:"warnings"` +} + // One model provider available to the session — the model analog of the account // `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never // branches on kind. @@ -9672,6 +10586,126 @@ type ModelProviderDescriptor struct { Ordering int64 `json:"ordering"` } +// Provider discovery parameters. +// Experimental: ModelProviderDiscoverRequest is part of an experimental API and may change +// or be removed. +type ModelProviderDiscoverRequest struct { + // Opaque adapter identity returned by `session.providers.getCatalog`. + AdapterID string `json:"adapterId"` + // Provider-specific JSON input. Omission or null selects adapter defaults unless + // requiresInput is true. Non-null input is validated against the advertised Draft 7 schema + // when present; otherwise validation belongs to the adapter. + Input any `json:"input,omitempty"` +} + +// Provider instances found by a discovery operation. +// Experimental: ModelProviderDiscoverResult is part of an experimental API and may change +// or be removed. +type ModelProviderDiscoverResult struct { + // Discovered provider instances. Empty when passive default discovery finds no reachable + // provider. + Instances []ModelProviderInstance `json:"instances"` + // Typed operation outcome. Passive discovery can return `absent` with an empty instance + // list. + Outcome ModelProviderOperationOutcome `json:"outcome"` +} + +// Provider status request parameters. +// Experimental: ModelProviderGetStatusRequest is part of an experimental API and may change +// or be removed. +type ModelProviderGetStatusRequest struct { + // Provider instance reference returned by discovery. + Instance ModelProviderInstanceReference `json:"instance"` +} + +// A normalized model-provider instance discovered by the runtime. +// Experimental: ModelProviderInstance is part of an experimental API and may change or be +// removed. +type ModelProviderInstance struct { + // Human-readable instance name. + DisplayName string `json:"displayName"` + // Inference API endpoint when the provider exposes one separately from its management + // endpoint. + InferenceEndpoint *string `json:"inferenceEndpoint,omitempty"` + // Transport to use for inference against this instance. + InferenceTransport *ProviderEndpointTransport `json:"inferenceTransport,omitempty"` + // Provider family to use for inference against this instance. + InferenceType *ProviderEndpointType `json:"inferenceType,omitempty"` + // Wire API to use for inference against this instance, when required by the provider family. + InferenceWireAPI *ProviderEndpointWireAPI `json:"inferenceWireApi,omitempty"` + // Attribution for the adapter that produced this instance. + Provenance ModelProviderProvenance `json:"provenance"` + // Self-contained reference for subsequent provider operations. + Reference ModelProviderInstanceReference `json:"reference"` +} + +// Serializable reference to a discovered provider instance. +// Experimental: ModelProviderInstanceReference is part of an experimental API and may +// change or be removed. +type ModelProviderInstanceReference struct { + // Stable opaque identity of the adapter that owns this reference. Must be present in the + // target session's effective catalog. + AdapterID string `json:"adapterId"` + // Stable instance identifier derived by the provider adapter, such as + // `ollama:{normalizedEndpoint}`. + ID string `json:"id"` + // Absolute provider management URI. The adapter validates normalization, supported schemes, + // and permission to access it against its bound configuration; a reference does not grant + // authority. + ManagementEndpoint string `json:"managementEndpoint"` + // Descriptive provider family. Must match the selected adapter; not a routing key. + ProviderKind string `json:"providerKind"` +} + +// Provider model inventory request parameters. +// Experimental: ModelProviderModelsListRequest is part of an experimental API and may +// change or be removed. +type ModelProviderModelsListRequest struct { + // Provider instance reference returned by discovery. + Instance ModelProviderInstanceReference `json:"instance"` +} + +// Typed provider-operation outcome. Use the code for control flow and the optional message +// for display. +// Experimental: ModelProviderOperationOutcome is part of an experimental API and may change +// or be removed. +type ModelProviderOperationOutcome struct { + // Machine-readable operation outcome. + Code ModelProviderOperationOutcomeCode `json:"code"` + // Human-readable detail for non-success outcomes. + Message *string `json:"message,omitempty"` +} + +// A discovered instance and one of its models to translate into provider configuration. +// Pass back the instance and model as returned by `session.providers.discover` and +// `session.providers.models.list`. +// Experimental: ModelProviderPrepareConfigurationRequest is part of an experimental API and +// may change or be removed. +type ModelProviderPrepareConfigurationRequest struct { + // The discovered instance that serves the model. + Instance ModelProviderInstance `json:"instance"` + // The discovered model to configure. + Model DiscoveredModel `json:"model"` +} + +// Attribution for the adapter that produced a provider row. +// Experimental: ModelProviderProvenance is part of an experimental API and may change or be +// removed. +type ModelProviderProvenance struct { + // Stable opaque adapter identity from the effective catalog. Treat this as a whole + // identifier, not a parseable owner or kind. + AdapterID string `json:"adapterId"` + // Human-readable contributor name, not the adapter display name. + OwnerDisplayName *string `json:"ownerDisplayName,omitempty"` + // Stable contributor identifier when the adapter has an owner outside the runtime. + // Independent of the contribution mechanism and not a routing key. + OwnerID *string `json:"ownerId,omitempty"` + // Descriptive provider family that produced this row; not a routing key. + ProviderKind string `json:"providerKind"` + // Kind of component that supplied the adapter. + Source ModelProviderProvenanceSource `json:"source"` +} + // A neutral reference to the model provider that produced a model: an opaque id, a // human-readable label, and the provider kind. Carried on each enumerated Model so // consumers can group by provider without reaching into a provider-shaped internal type. @@ -9688,6 +10722,30 @@ type ModelProviderRef struct { Label string `json:"label"` } +// Current health information for a provider instance. +// Experimental: ModelProviderStatus is part of an experimental API and may change or be +// removed. +type ModelProviderStatus struct { + // Normalized provider instance. + Instance ModelProviderInstance `json:"instance"` + // Typed operation outcome. + Outcome ModelProviderOperationOutcome `json:"outcome"` + // Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + Status string `json:"status"` + // Provider-reported version. + Version *string `json:"version,omitempty"` +} + +// A non-fatal provider observation warning. +// Experimental: ModelProviderWarning is part of an experimental API and may change or be +// removed. +type ModelProviderWarning struct { + // Machine-readable warning code. + Code string `json:"code"` + // Human-readable warning message. + Message string `json:"message"` +} + // Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are // intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs // remain exempt from repository-only policy but are restricted by this host list. Omit or @@ -9965,6 +11023,9 @@ type NamedProviderConfig struct { HasBearerTokenProvider *bool `json:"hasBearerTokenProvider,omitempty"` // Additional HTTP headers included with provider requests. Headers map[string]string `json:"headers,omitzero"` + // The product serving the provider's models, reported in telemetry as `model_provider`. + // Only affects telemetry. + ModelProvider *ProviderConfigModelProvider `json:"modelProvider,omitempty"` // Unique provider name used to qualify model selection IDs. Name string `json:"name"` // Transport used to communicate with the provider. @@ -11926,6 +12987,10 @@ type ProviderConfig struct { // Well-known model ID used for capability lookup. When set, agent behavior config and token // limits are inferred from this model. ModelID *string `json:"modelId,omitempty"` + // The product serving the model, reported in telemetry as `model_provider`. Set it when + // `type` alone cannot identify the product, such as Ollama or LM Studio behind an + // OpenAI-compatible endpoint. Only affects telemetry. + ModelProvider *ProviderConfigModelProvider `json:"modelProvider,omitempty"` // Provider name used for model and telemetry attribution. ProviderName *string `json:"providerName,omitempty"` // Provider transport. Defaults to "http". @@ -13453,6 +14518,41 @@ type SandboxMaskedEnvVar struct { InjectHosts []string `json:"injectHosts"` } +// Result of creating the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaCreateResult is part of an experimental API and may change or +// be removed. +type SandboxProxyCaCreateResult struct { + // Absolute path of the public certificate of the certificate authority, in PEM format. + CertificatePath string `json:"certificatePath"` +} + +// Identifies the credential hosts that the persistent certificate authority of the sandbox +// credential proxy must cover. The runtime always adds the hosts from the saved user +// settings. +// Experimental: SandboxProxyCaRequest is part of an experimental API and may change or be +// removed. +type SandboxProxyCaRequest struct { + // The sandbox configuration that the host gives its sessions. The runtime reads the + // credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` +} + +// Status of the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaStatus is part of an experimental API and may change or be +// removed. +type SandboxProxyCaStatus struct { + // Whether this process can add the certificate authority to OS trust without credentials + // from a different user. False where OS trust is unsupported, and on Windows when the + // process cannot elevate itself to write the machine trust store. When false, do not offer + // to set up the certificate authority. + CanInstall bool `json:"canInstall"` + // Human-readable reason for the state. On `installed` or `notInstalled`, present only when + // the certificate authority must be rotated, and then says why. + Detail *string `json:"detail,omitempty"` + // The state of the certificate authority. + State SandboxProxyCaState `json:"state"` +} + // Register an absolute-time scheduled prompt. // Experimental: ScheduleAddAtRequest is part of an experimental API and may change or be // removed. @@ -13864,24 +14964,10 @@ type SessionAgentListRequest struct { type SessionAgentSetPromptResult struct { } -// Credential-free authentication identity safe to expose to hosts and user interfaces. +// Current authentication information, or null when no authentication is active. // Experimental: SessionAuthInfoResult is part of an experimental API and may change or be // removed. -type SessionAuthInfoResult struct { - // Snapshot of the authenticated user's Copilot subscription info, if known - CopilotUser *CopilotUserResponse `json:"copilotUser,omitempty"` - // Name of the environment variable that supplied the credential, when applicable - EnvVar *string `json:"envVar,omitempty"` - // Authentication host - 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"` -} +type SessionAuthInfoResult = *AuthIdentity // Internal GitHub login parameters. // Experimental: SessionAuthLoginRequest is part of an experimental API and may change or be @@ -13993,6 +15079,16 @@ type SessionCompletionItem struct { RangeStart *int64 `json:"rangeStart,omitempty"` } +// The IDE a host is connected to, as reported to the session. +// Experimental: SessionConnectedIdeInfo is part of an experimental API and may change or be +// removed. +type SessionConnectedIdeInfo struct { + // Display name of the connected IDE, for example `VS Code`. + IdeName string `json:"ideName"` + // Absolute path of the workspace folder the IDE has open. + WorkspaceFolder string `json:"workspaceFolder"` +} + // Pre-resolved working-directory context for session startup. // Experimental: SessionContext is part of an experimental API and may change or be removed. type SessionContext struct { @@ -14013,7 +15109,8 @@ type SessionContext struct { // Experimental: SessionContextAttribution is part of an experimental API and may change or // be removed. type SessionContextAttribution struct { - // Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors + // Output reservation overlapping the displayed prompt allowance plus the tokens past the + // effective input budget's buffer-exhaustion blocking threshold. Mirrors // `SessionContextInfo.bufferTokens`. BufferTokens int64 `json:"bufferTokens"` // The six normalized `/context` header buckets, computed from the same tokenization as @@ -14029,9 +15126,9 @@ type SessionContextAttribution struct { // Flat list of per-source attribution entries. Group by `kind` and render unrecognized // kinds generically. Nesting and rollups are expressed via `parentId`. Entries []SessionContextAttributionEntriesItem `json:"entries"` - // Prompt limit plus the model's output reserve: the full context window - // `categories.freeSpace` and `categories.buffer` are measured against. Mirrors - // `SessionContextInfo.limit`. + // Advertised prompt allowance for the selected context tier: the denominator for + // context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. + // Mirrors `SessionContextInfo.limit`. Limit int64 `json:"limit"` // The concrete model id the entire breakdown was tokenized against (feeds the per-model // token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the @@ -14042,8 +15139,8 @@ type SessionContextAttribution struct { // `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected // model), `default` (a fallback before any model is known). ModelSource string `json:"modelSource"` - // Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` - // context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + // Effective input budget after reserving requested output against the combined context + // ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. PromptTokenLimit int64 `json:"promptTokenLimit"` // Total token count of the current context window the entries are measured against (system // message + conversation messages + tool definitions — the same total reported by @@ -14056,7 +15153,7 @@ type SessionContextAttribution struct { // describe window capacity rather than occupied context, so the values do not sum to // `totalTokens`. type SessionContextAttributionCategories struct { - // Output reserve plus post-blocking-threshold buffer. + // Overlapping output reservation plus post-blocking-threshold buffer. Buffer int64 `json:"buffer"` // Custom-instructions tokens (0 when none are configured). CustomInstructions int64 `json:"customInstructions"` @@ -14106,21 +15203,25 @@ type SessionContextAttributionEntriesItem struct { // Experimental: SessionContextInfo is part of an experimental API and may change or be // removed. type SessionContextInfo struct { - // Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + // Output reservation overlapping the displayed prompt allowance plus tokens after the + // effective input budget's buffer-exhaustion blocking threshold (default 95%). BufferTokens int64 `json:"bufferTokens"` // Token count at which background compaction starts (configurable percentage of // promptTokenLimit) CompactionThreshold int64 `json:"compactionThreshold"` // Tokens consumed by user/assistant/tool messages ConversationTokens int64 `json:"conversationTokens"` - // Prompt token limit plus the model's full output token limit. + // Advertised prompt allowance for the selected context tier, without adding output tokens. + // The denominator for context-usage displays. Limit int64 `json:"limit"` // Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes // deferred tools) MCPToolsTokens int64 `json:"mcpToolsTokens"` // The model used for token counting ModelName string `json:"modelName"` - // Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + // Effective input budget: the selected tier's prompt allowance bounded by the combined + // context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when + // limits are unspecified. PromptTokenLimit int64 `json:"promptTokenLimit"` // Tokens consumed by the system prompt SystemTokens int64 `json:"systemTokens"` @@ -14182,6 +15283,9 @@ type SessionFSError struct { Code SessionFSErrorCode `json:"code"` // Free-form detail about the error, for logging/diagnostics Message *string `json:"message,omitempty"` + // For failed writeFile requests only: true if the provider changed the target before + // failing. Omit when unknown or unchanged. + WriteChanged *bool `json:"writeChanged,omitempty"` } // Path to test for existence in the client-provided session filesystem. @@ -14270,6 +15374,26 @@ type SessionFSReaddirWithTypesResult struct { Error *SessionFSError `json:"error,omitempty"` } +// Path of the binary file to read from the client-provided session filesystem. +// Experimental: SessionFSReadFileBytesRequest is part of an experimental API and may change +// or be removed. +type SessionFSReadFileBytesRequest struct { + // Path using SessionFs conventions + Path string `json:"path"` + // Target session identifier + SessionID string `json:"sessionId"` +} + +// File bytes as standard base64, or a filesystem error if the read failed. +// Experimental: SessionFSReadFileBytesResult is part of an experimental API and may change +// or be removed. +type SessionFSReadFileBytesResult struct { + // Exact file bytes encoded as standard base64 + Content string `json:"content"` + // Describes a filesystem error. + Error *SessionFSError `json:"error,omitempty"` +} + // Path of the file to read from the client-provided session filesystem. // Experimental: SessionFSReadFileRequest is part of an experimental API and may change or // be removed. @@ -14322,6 +15446,9 @@ type SessionFSRmRequest struct { // Experimental: SessionFSSetProviderCapabilities is part of an experimental API and may // change or be removed. type SessionFSSetProviderCapabilities struct { + // Whether the provider supports binary reads and writes through sessionFs.readFileBytes and + // sessionFs.writeFileBytes + Binary *bool `json:"binary,omitempty"` // Whether the provider supports SQLite query/exists operations Sqlite *bool `json:"sqlite,omitempty"` } @@ -14476,6 +15603,21 @@ type SessionFSStatResult struct { Size int64 `json:"size"` } +// File path, standard-base64-encoded bytes to write, and optional mode for the +// client-provided session filesystem. +// Experimental: SessionFSWriteFileBytesRequest is part of an experimental API and may +// change or be removed. +type SessionFSWriteFileBytesRequest struct { + // Exact file bytes encoded as standard base64 + Content string `json:"content"` + // Optional POSIX-style mode for newly created files + Mode *int64 `json:"mode,omitempty"` + // Path using SessionFs conventions + Path string `json:"path"` + // Target session identifier + SessionID string `json:"sessionId"` +} + // File path, content to write, and optional mode for the client-provided session filesystem. // Experimental: SessionFSWriteFileRequest is part of an experimental API and may change or // be removed. @@ -14616,11 +15758,6 @@ type SessionInstalledPluginSourceURL struct { URL string `json:"url"` } -// Experimental: SessionInstructionsReloadResult is part of an experimental API and may -// change or be removed. -type SessionInstructionsReloadResult struct { -} - // Baseline data provenance for a prediction. // Experimental: SessionLimitPredictionBaselineData is part of an experimental API and may // change or be removed. @@ -14844,6 +15981,11 @@ type SessionManagedPermissions struct { // 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"` + // Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or + // `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected + // because every network request must be enforceable at host-level egress. Multiple managed + // sources intersect their lists; an empty list denies all hosts. + LimitTo []string `json:"limitTo,omitzero"` } // Managed settings an SDK host may inject at session startup. Only permissions are accepted @@ -14899,6 +16041,11 @@ type SessionMCPOauthCancelLoginResult struct { Cancelled bool `json:"cancelled"` } +// Experimental: SessionMCPOauthCompleteResult is part of an experimental API and may change +// or be removed. +type SessionMCPOauthCompleteResult struct { +} + // Effect-free preparation bound to the existing local session, requester and installation, // with frozen options. // Experimental: SessionMCPOauthPrepareLoginRequest is part of an experimental API and may @@ -14942,6 +16089,20 @@ type SessionMCPReloadResult struct { type SessionMCPRestartServerResult struct { } +// Records which IDE the host is connected to, or clears it. +// Experimental: SessionMCPSetConnectedIdeInfoParams is part of an experimental API and may +// change or be removed. +type SessionMCPSetConnectedIdeInfoParams struct { + // The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports + // that it is disconnected. + Ide *SessionConnectedIdeInfo `json:"ide,omitempty"` +} + +// Experimental: SessionMCPSetConnectedIdeInfoResult is part of an experimental API and may +// change or be removed. +type SessionMCPSetConnectedIdeInfoResult struct { +} + // Experimental: SessionMCPStartServerResult is part of an experimental API and may change // or be removed. type SessionMCPStartServerResult struct { @@ -15770,6 +16931,35 @@ type SessionsCloseResult struct { type SessionsConfigureSessionExtensionsResult struct { } +// Identity, state location and starting context for a workspace record. +// Experimental: SessionsCreateWorkspaceRequest is part of an experimental API and may +// change or be removed. +type SessionsCreateWorkspaceRequest struct { + // Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, + // `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, + // `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + Context *SessionWorkingDirectoryContextWithClient `json:"context,omitempty"` + // `windows` (any letter case) selects Windows path rules. Any other value selects POSIX + // path rules. + Convention string `json:"convention"` + // User-supplied display name for the workspace + Name *string `json:"name,omitempty"` + // Session ID the workspace record belongs to + SessionID string `json:"sessionId"` + // Directory the session's state is written under when no session filesystem provider is + // configured. Ignored when a provider is configured; the provider's session state path is + // used instead. + SessionStatePath string `json:"sessionStatePath"` +} + +// The workspace record that was written. +// Experimental: SessionsCreateWorkspaceResult is part of an experimental API and may change +// or be removed. +type SessionsCreateWorkspaceResult struct { + // The created workspace record, as JSON + WorkspaceJSON string `json:"workspaceJson"` +} + // Session ID to delete from disk. // Experimental: SessionsDeleteRequest is part of an experimental API and may change or be // removed. @@ -16206,6 +17396,25 @@ type SessionsLoadDeferredRepoHooksRequest struct { SessionID string `json:"sessionId"` } +// Where the session's state lives, as a root directory and the session ID under it. +// Experimental: SessionsLoadWorkspaceRequest is part of an experimental API and may change +// or be removed. +type SessionsLoadWorkspaceRequest struct { + // Session ID naming the state directory under the sessions home. Rejected when it is + // absolute or contains a parent component, so it cannot escape the sessions home. + SessionID string `json:"sessionId"` + // Root directory every session's state directory sits under + SessionsHome string `json:"sessionsHome"` +} + +// The workspace record on disk, omitted when the session has none. +// Experimental: SessionsLoadWorkspaceResult is part of an experimental API and may change +// or be removed. +type SessionsLoadWorkspaceResult struct { + // The workspace record, as JSON. Omitted when the record does not exist. + WorkspaceJSON *string `json:"workspaceJson,omitempty"` +} + // `sessions.open` handoff progress update with step, status, and optional message. // Experimental: SessionsOpenProgress is part of an experimental API and may change or be // removed. @@ -16361,6 +17570,30 @@ type SessionsTransferRemoteControlRequest struct { ToSessionID string `json:"toSessionId"` } +// Where the session's state lives, plus workspace-schema fields to merge into its workspace +// record. Stored keys outside the schema are not preserved, and a stored `fork_count` is +// never replaced. +// Experimental: SessionsUpdateWorkspaceFieldsRequest is part of an experimental API and may +// change or be removed. +type SessionsUpdateWorkspaceFieldsRequest struct { + // Workspace-schema fields to merge into the record, as a JSON object. Fields the object + // omits keep their stored values, except stored keys outside the schema are not preserved + // and a stored `fork_count` is never replaced. + FieldsJSON string `json:"fieldsJson"` + // Session ID naming the state directory under the sessions home. Rejected when it is + // absolute or contains a parent component, so it cannot escape the sessions home. + SessionID string `json:"sessionId"` + // Root directory every session's state directory sits under + SessionsHome string `json:"sessionsHome"` +} + +// The merge completed. The record carries the supplied workspace-schema fields, but a +// stored `fork_count` stays. +// Experimental: SessionsUpdateWorkspaceFieldsResult is part of an experimental API and may +// change or be removed. +type SessionsUpdateWorkspaceFieldsResult struct { +} + // Experimental: SessionSuspendResult is part of an experimental API and may change or be // removed. type SessionSuspendResult struct { @@ -16585,6 +17818,30 @@ type SessionWorkingDirectoryContext struct { RepositoryHost *string `json:"repositoryHost,omitempty"` } +// A working-directory context together with the client that produced it. +// Experimental: SessionWorkingDirectoryContextWithClient is part of an experimental API and +// may change or be removed. +type SessionWorkingDirectoryContextWithClient struct { + // Merge-base commit SHA + BaseCommit *string `json:"baseCommit,omitempty"` + // Current git branch name + Branch *string `json:"branch,omitempty"` + // Name of the client that created the session + ClientName *string `json:"clientName,omitempty"` + // Current working directory path + Cwd string `json:"cwd"` + // Root directory of the git repository + GitRoot *string `json:"gitRoot,omitempty"` + // Head commit of the current git branch + HeadCommit *string `json:"headCommit,omitempty"` + // Hosting platform type of the repository + HostType *string `json:"hostType,omitempty"` + // Repository identifier derived from the git remote URL + Repository *string `json:"repository,omitempty"` + // Raw host string from the git remote URL + RepositoryHost *string `json:"repositoryHost,omitempty"` +} + // Experimental: SessionWorkspacesCreateDirectoryResult is part of an experimental API and // may change or be removed. type SessionWorkspacesCreateDirectoryResult struct { @@ -16623,6 +17880,10 @@ func (RawSettableAuthInfoData) settableAuthInfo() {} func (r RawSettableAuthInfoData) settableAuthInfoType() SettableAuthInfoType { return r.Discriminator } +func (AccountAuthInfo) settableAuthInfo() {} +func (AccountAuthInfo) settableAuthInfoType() SettableAuthInfoType { + return SettableAuthInfoTypeAccount +} func (APIKeyAuthInfo) settableAuthInfo() {} func (APIKeyAuthInfo) settableAuthInfoType() SettableAuthInfoType { return SettableAuthInfoTypeAPIKey @@ -16724,7 +17985,8 @@ type ShellCredentials struct { Git *bool `json:"git,omitempty"` } -// Shell command to run, with optional working directory and timeout in milliseconds. +// Shell command to run, with optional working directory and timeout in milliseconds. Spawn +// failures return an RPC error. // Experimental: ShellExecRequest is part of an experimental API and may change or be // removed. type ShellExecRequest struct { @@ -16736,11 +17998,11 @@ type ShellExecRequest struct { Timeout *int64 `json:"timeout,omitempty"` } -// Identifier of the spawned process, used to correlate streamed output and exit -// notifications. +// Identifier of the spawned shell process, usable with shell.kill while the process is +// running. // Experimental: ShellExecResult is part of an experimental API and may change or be removed. type ShellExecResult struct { - // Unique identifier for tracking streamed output + // Identifier usable with shell.kill while the process is running ProcessID string `json:"processId"` } @@ -17508,8 +18770,8 @@ type SkillPlanUninstallRequest struct { PolicySessionID string `json:"policySessionId"` } -// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched -// separately and lazily. +// Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is +// fetched separately and lazily. // Experimental: SkillProviderDescriptor is part of an experimental API and may change or be // removed. type SkillProviderDescriptor struct { @@ -17557,15 +18819,18 @@ type SkillProviderReadRequest struct { SessionID string `json:"sessionId"` } -// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related -// files and assets are not supported. +// Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter +// is optional: fields it omits come from the catalog descriptor, fields it declares must +// match the descriptor, and `allowed-tools` is read only from frontmatter. Related files +// and assets are not supported. // Experimental: SkillProviderReadResult is part of an experimental API and may change or be // removed. // Internal: SkillProviderReadResult is an internal SDK API and is not part of the public // surface. type SkillProviderReadResult struct { - // Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. - Markdown string `json:"markdown"` + // SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill + // with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. + Markdown *string `json:"markdown"` } // Skill names to mark as disabled in global configuration, replacing any previous list. @@ -19486,9 +20751,8 @@ type UserSettingMetadata struct { Value any `json:"value"` } -// Per-key metadata for every known user setting (settings.json overlaid with the legacy -// config.json, config.json wins), including settings left at their default. Excludes -// repository- and enterprise-managed overrides. +// Per-key metadata for every known user setting in settings.json, including settings left +// at their default. Excludes repository- and enterprise-managed overrides. // Experimental: UserSettingsGetResult is part of an experimental API and may change or be // removed. type UserSettingsGetResult struct { @@ -19497,11 +20761,6 @@ type UserSettingsGetResult struct { Settings map[string]UserSettingMetadata `json:"settings"` } -// Experimental: UserSettingsReloadResult is part of an experimental API and may change or -// be removed. -type UserSettingsReloadResult struct { -} - // Partial user settings to write to settings.json. Each top-level key is written // individually, replacing the existing value; a key whose value is null is removed. // Experimental: UserSettingsSetRequest is part of an experimental API and may change or be @@ -19511,14 +20770,9 @@ type UserSettingsSetRequest struct { Settings any `json:"settings"` } -// Outcome of writing user settings. // Experimental: UserSettingsSetResult is part of an experimental API and may change or be // removed. type UserSettingsSetResult struct { - // Top-level keys whose write landed in settings.json but is shadowed by a value still - // present in the legacy config.json (config.json wins on read). The write does not take - // effect until the legacy value is removed. - ShadowedKeys []string `json:"shadowedKeys"` } // The approval to add as a session-scoped rule @@ -21112,6 +22366,7 @@ const ( type AuthInfoType string const ( + AuthInfoTypeAccount AuthInfoType = "account" AuthInfoTypeAPIKey AuthInfoType = "api-key" AuthInfoTypeCopilotAPIToken AuthInfoType = "copilot-api-token" AuthInfoTypeEnv AuthInfoType = "env" @@ -21122,16 +22377,19 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) -// Terminal disposition of a login persistence attempt. +// Disposition of a login attempt, including pending user decisions. // Experimental: AuthLoginResultStatus is part of an experimental API and may change or be // removed. type AuthLoginResultStatus string const ( - // The credential was persisted and the account is signed in. + // The credential was persisted and the selected account is signed in. AuthLoginResultStatusCompleted AuthLoginResultStatus = "completed" // The user declined plaintext persistence. AuthLoginResultStatusDeclined AuthLoginResultStatus = "declined" + // Credentials are saved; select an account using a returned selectionId as advance input to + // complete sign-in. + AuthLoginResultStatusNeedsAccountSelection AuthLoginResultStatus = "needs-account-selection" // Persistence needs explicit consent to store the token in plaintext. AuthLoginResultStatusNeedsPlaintextConsent AuthLoginResultStatus = "needs-plaintext-consent" ) @@ -21944,6 +23202,20 @@ const ( ConnectorConnectResultKindPending ConnectorConnectResultKind = "pending" ) +// Availability. +// Experimental: ConnectorDiscoveryAvailability is part of an experimental API and may +// change or be removed. +type ConnectorDiscoveryAvailability string + +const ( + // Disabled. + ConnectorDiscoveryAvailabilityDisabled ConnectorDiscoveryAvailability = "disabled" + // Enabled. + ConnectorDiscoveryAvailabilityEnabled ConnectorDiscoveryAvailability = "enabled" + // Unavailable. + ConnectorDiscoveryAvailabilityUnavailable ConnectorDiscoveryAvailability = "unavailable" +) + // Live MCP status of one Connector-owned runtime server. // Experimental: ConnectorMCPStatus is part of an experimental API and may change or be // removed. @@ -22000,6 +23272,44 @@ const ( CopilotAPITokenAuthInfoHostHTTPSGitHubCom CopilotAPITokenAuthInfoHost = "https://github.com" ) +// Result of reloading a customization component. +// Experimental: CustomizationReloadStatus is part of an experimental API and may change or +// be removed. +type CustomizationReloadStatus string + +const ( + // The component could not be refreshed; other components may still reload. + CustomizationReloadStatusFailed CustomizationReloadStatus = "failed" + // The component was refreshed successfully. + CustomizationReloadStatusReloaded CustomizationReloadStatus = "reloaded" + // The component was not configured, loaded, or eligible for refresh. + CustomizationReloadStatusSkipped CustomizationReloadStatus = "skipped" +) + +// Component of session customization discovery. +// Experimental: CustomizationReloadSubsystem is part of an experimental API and may change +// or be removed. +type CustomizationReloadSubsystem string + +const ( + // Discovered custom agents. + CustomizationReloadSubsystemAgents CustomizationReloadSubsystem = "agents" + // Configured session extensions. + CustomizationReloadSubsystemExtensions CustomizationReloadSubsystem = "extensions" + // Configured session and plugin hooks. + CustomizationReloadSubsystemHooks CustomizationReloadSubsystem = "hooks" + // Session instructions and their cached dynamic context. + CustomizationReloadSubsystemInstructions CustomizationReloadSubsystem = "instructions" + // Loaded MCP server configuration. + CustomizationReloadSubsystemMCP CustomizationReloadSubsystem = "mcp" + // Discovered plugin configuration. + CustomizationReloadSubsystemPlugins CustomizationReloadSubsystem = "plugins" + // Repository metadata and working-directory context. + CustomizationReloadSubsystemRepositoryContext CustomizationReloadSubsystem = "repositoryContext" + // Discovered skills. + CustomizationReloadSubsystemSkills CustomizationReloadSubsystem = "skills" +) + // Kind discriminator for DebugCollectLogsDestination. type DebugCollectLogsDestinationKind string @@ -22174,31 +23484,6 @@ const ( DiscoveredMCPServerTypeStdio DiscoveredMCPServerType = "stdio" ) -// Status discriminator for EntraTokenAcquireResult. -// Experimental: EntraTokenAcquireResultStatus is part of an experimental API and may change -// or be removed. -type EntraTokenAcquireResultStatus string - -const ( - EntraTokenAcquireResultStatusInteractionRequired EntraTokenAcquireResultStatus = "interaction-required" - EntraTokenAcquireResultStatusOk EntraTokenAcquireResultStatus = "ok" -) - -// How far OneAuth may go to acquire the requested token. -// Experimental: EntraTokenInteraction is part of an experimental API and may change or be -// removed. -type EntraTokenInteraction string - -const ( - // Always prompt interactively, bypassing any cached or silently-refreshable token. - EntraTokenInteractionForceInteractive EntraTokenInteraction = "force-interactive" - // Allow interactive acquisition, prompting the user only when a cached or silent token is - // unavailable. - EntraTokenInteractionInteractive EntraTokenInteraction = "interactive" - // Acquire the token without any user interaction, failing if interaction would be required. - EntraTokenInteractionSilent EntraTokenInteraction = "silent" -) - // GitHub Mission Control compute kind. // Experimental: EnvironmentKind is part of an experimental API and may change or be removed. type EnvironmentKind string @@ -23343,6 +24628,17 @@ const ( MCPPlanValueCategoryURLVariable MCPPlanValueCategory = "url-variable" ) +// The sender role of an MCP prompt message. +// Experimental: MCPPromptRole is part of an experimental API and may change or be removed. +type MCPPromptRole string + +const ( + // A message from the assistant. + MCPPromptRoleAssistant MCPPromptRole = "assistant" + // A message from the user. + MCPPromptRoleUser MCPPromptRole = "user" +) + // Outcome of the sampling inference. 'success' produced a response; 'failure' encountered // an error (including agent-side rejection by content filter or criteria); 'cancelled' the // caller cancelled this execution via cancelSamplingExecution. @@ -23457,11 +24753,13 @@ const ( MCPServerConfigStdioTypeStdio MCPServerConfigStdioType = "stdio" ) -// Configuration source: user, workspace, plugin, builtin, or managed +// Configuration source: user, workspace, plugin, builtin, managed, or account // Experimental: MCPServerSource is part of an experimental API and may change or be removed. type MCPServerSource string const ( + // Server contributed by a signed-in account; enablement and organization policy still apply. + MCPServerSourceAccount MCPServerSource = "account" // Server bundled with the runtime. MCPServerSourceBuiltin MCPServerSource = "builtin" // Server supplied by a trusted host-managed catalog. @@ -23634,6 +24932,53 @@ const ( ModelPolicyStateUnconfigured ModelPolicyState = "unconfigured" ) +// When the runtime may run an adapter without an explicit user action. +// Experimental: ModelProviderAutomaticDiscoveryMode is part of an experimental API and may +// change or be removed. +type ModelProviderAutomaticDiscoveryMode string + +const ( + // The adapter declares that automatic discovery is safe when the other policy fields are + // satisfied. + ModelProviderAutomaticDiscoveryModeAutomatic ModelProviderAutomaticDiscoveryMode = "automatic" + // The adapter may refresh instances the user already configured, but must not scan for new + // instances automatically. + ModelProviderAutomaticDiscoveryModeConfiguredOnly ModelProviderAutomaticDiscoveryMode = "configuredOnly" + // The adapter must run only after an explicit user action. + ModelProviderAutomaticDiscoveryModeExplicit ModelProviderAutomaticDiscoveryMode = "explicit" +) + +// Whether a planned configuration entry is new or already present in the session registry. +// Experimental: ModelProviderConfigurationDisposition is part of an experimental API and +// may change or be removed. +type ModelProviderConfigurationDisposition string + +const ( + // An equivalent entry is already registered; the caller should reuse it rather than adding + // a duplicate. + ModelProviderConfigurationDispositionAlreadyConfigured ModelProviderConfigurationDisposition = "alreadyConfigured" + // No matching entry is registered; the caller should add the entry. + ModelProviderConfigurationDispositionCreate ModelProviderConfigurationDisposition = "create" +) + +// Network reach an adapter may use during discovery. +// Experimental: ModelProviderDiscoveryNetworkScope is part of an experimental API and may +// change or be removed. +type ModelProviderDiscoveryNetworkScope string + +const ( + // Discovery contacts only endpoints the user already configured. + ModelProviderDiscoveryNetworkScopeConfiguredEndpointOnly ModelProviderDiscoveryNetworkScope = "configuredEndpointOnly" + // Discovery may contact remote internet services. + ModelProviderDiscoveryNetworkScopeInternet ModelProviderDiscoveryNetworkScope = "internet" + // Discovery may scan or contact the local network. + ModelProviderDiscoveryNetworkScopeLocalNetwork ModelProviderDiscoveryNetworkScope = "localNetwork" + // Discovery is limited to loopback addresses on the local machine. + ModelProviderDiscoveryNetworkScopeLoopbackOnly ModelProviderDiscoveryNetworkScope = "loopbackOnly" + // Discovery does not contact a network service. + ModelProviderDiscoveryNetworkScopeNone ModelProviderDiscoveryNetworkScope = "none" +) + // The neutral kind of a model provider — the model analog of `AccountKind`. A model // provider is the live, entitled source a model came from; central code never branches on // this beyond a single dispatch. @@ -23650,6 +24995,40 @@ const ( ModelProviderKindLoki ModelProviderKind = "loki" ) +// Typed outcome for a provider operation. +// Experimental: ModelProviderOperationOutcomeCode is part of an experimental API and may +// change or be removed. +type ModelProviderOperationOutcomeCode string + +const ( + // The provider or instance is absent during discovery, status, or model listing. Distinct + // from a successful empty inventory. + ModelProviderOperationOutcomeCodeAbsent ModelProviderOperationOutcomeCode = "absent" + // The operation failed for a reason other than absence or reachability. + ModelProviderOperationOutcomeCodeFailed ModelProviderOperationOutcomeCode = "failed" + // The operation completed successfully; an empty inventory is valid. + ModelProviderOperationOutcomeCodeSuccess ModelProviderOperationOutcomeCode = "success" + // The provider is configured or expected but could not be reached. + ModelProviderOperationOutcomeCodeUnreachable ModelProviderOperationOutcomeCode = "unreachable" +) + +// Kind of component that supplied a provider adapter or row. Attribution does not confer +// authority. +// Experimental: ModelProviderProvenanceSource is part of an experimental API and may change +// or be removed. +type ModelProviderProvenanceSource string + +const ( + // Built into the runtime. + ModelProviderProvenanceSourceBuiltIn ModelProviderProvenanceSource = "builtIn" + // Derived from existing user configuration. + ModelProviderProvenanceSourceConfigured ModelProviderProvenanceSource = "configured" + // Supplied by another trusted contributor. + ModelProviderProvenanceSourceCustom ModelProviderProvenanceSource = "custom" + // Supplied by an extension. + ModelProviderProvenanceSourceExtension ModelProviderProvenanceSource = "extension" +) + // Whether the requested preference was already effective or was accepted for later // transactional activation. // Experimental: ModelSwitchAutoTierStatus is part of an experimental API and may change or @@ -24166,6 +25545,28 @@ const ( ProtocolSystemMessageConfigModeReplace ProtocolSystemMessageConfigMode = "replace" ) +// The product serving the model, reported in telemetry as `model_provider`. +// Experimental: ProviderConfigModelProvider is part of an experimental API and may change +// or be removed. +type ProviderConfigModelProvider string + +const ( + // Anthropic API. + ProviderConfigModelProviderAnthropic ProviderConfigModelProvider = "anthropic" + // Azure OpenAI Service. + ProviderConfigModelProviderAzureOpenai ProviderConfigModelProvider = "azure_openai" + // Foundry Local. + ProviderConfigModelProviderFoundryLocal ProviderConfigModelProvider = "foundry_local" + // llama.cpp server. + ProviderConfigModelProviderLlamaCpp ProviderConfigModelProvider = "llama_cpp" + // LM Studio. + ProviderConfigModelProviderLmStudio ProviderConfigModelProvider = "lm_studio" + // Ollama. + ProviderConfigModelProviderOllama ProviderConfigModelProvider = "ollama" + // OpenAI API. + ProviderConfigModelProviderOpenai ProviderConfigModelProvider = "openai" +) + // Provider transport. Defaults to "http". // Experimental: ProviderConfigTransport is part of an experimental API and may change or be // removed. @@ -24415,6 +25816,23 @@ const ( SandboxConfigSourceUserEnabled SandboxConfigSource = "user_enabled" ) +// State of the persistent certificate authority of the sandbox credential proxy. +// Experimental: SandboxProxyCaState is part of an experimental API and may change or be +// removed. +type SandboxProxyCaState string + +const ( + // The runtime could not read the certificate authority or the OS trust store. + SandboxProxyCaStateError SandboxProxyCaState = "error" + // OS trust includes the stored certificate authority. + SandboxProxyCaStateInstalled SandboxProxyCaState = "installed" + // OS trust does not include the certificate authority, or none is stored. + SandboxProxyCaStateNotInstalled SandboxProxyCaState = "notInstalled" + // This platform has no supported OS trust store. The proxy uses a per-process certificate + // bundle. + SandboxProxyCaStateUnsupported SandboxProxyCaState = "unsupported" +) + // A session-scoped sandbox transition applied while handling a slash command // Experimental: SandboxSessionChange is part of an experimental API and may change or be // removed. @@ -24904,6 +26322,7 @@ const ( type SettableAuthInfoType string const ( + SettableAuthInfoTypeAccount SettableAuthInfoType = "account" SettableAuthInfoTypeAPIKey SettableAuthInfoType = "api-key" SettableAuthInfoTypeCopilotAPIToken SettableAuthInfoType = "copilot-api-token" SettableAuthInfoTypeEnv SettableAuthInfoType = "env" @@ -26013,6 +27432,82 @@ func (a *ServerCommandsAPI) List(ctx context.Context) (*CommandList, error) { return &result, nil } +// Experimental: ServerConnectorsAPI contains experimental APIs that may change or be +// removed. +type ServerConnectorsAPI serverAPI + +// GetAccounts returns eligible accounts. +// +// RPC method: connectors.getAccounts. +// +// Returns: Eligible accounts. +func (a *ServerConnectorsAPI) GetAccounts(ctx context.Context) (*ConnectorDiscoveryAccountList, error) { + raw, err := a.client.Request(ctx, "connectors.getAccounts", nil) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryAccountList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetCapabilities returns feature availability. +// +// RPC method: connectors.getCapabilities. +// +// Returns: Feature availability. +func (a *ServerConnectorsAPI) GetCapabilities(ctx context.Context) (*ConnectorDiscoveryCapabilities, error) { + raw, err := a.client.Request(ctx, "connectors.getCapabilities", nil) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCapabilities + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists entries for the selected account. +// +// RPC method: connectors.list. +// +// Parameters: Selected account. +// +// Returns: Entries for the selected account. +func (a *ServerConnectorsAPI) List(ctx context.Context, params *ConnectorDiscoveryAccountRequest) (*ConnectorDiscoveryCatalogResult, error) { + raw, err := a.client.Request(ctx, "connectors.list", params) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Refreshes entries for the selected account. +// +// RPC method: connectors.refresh. +// +// Parameters: Selected account. +// +// Returns: Entries for the selected account. +func (a *ServerConnectorsAPI) Refresh(ctx context.Context, params *ConnectorDiscoveryAccountRequest) (*ConnectorDiscoveryCatalogResult, error) { + raw, err := a.client.Request(ctx, "connectors.refresh", params) + if err != nil { + return nil, err + } + var result ConnectorDiscoveryCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerEnvironmentsAPI contains experimental APIs that may change or be // removed. type ServerEnvironmentsAPI serverAPI @@ -27203,6 +28698,133 @@ func (a *ServerSandboxAPI) GetHostSupport(ctx context.Context) (*SandboxHostSupp return &result, nil } +// Experimental: ServerSandboxProxyCaAPI contains experimental APIs that may change or be +// removed. +type ServerSandboxProxyCaAPI serverAPI + +// Creates the persistent certificate authority of the sandbox credential proxy if none is +// stored, without changing OS trust, and returns the path of its public certificate. Keeps +// an existing certificate authority, even one that must be rotated. Fails where OS trust is +// unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the +// saved user settings, so it refuses a certificate authority that also covers hosts from +// sandboxConfig. +// +// RPC method: sandbox.proxyCa.create. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Result of creating the persistent certificate authority of the sandbox +// credential proxy. +func (a *ServerSandboxProxyCaAPI) Create(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaCreateResult, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.create", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaCreateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetStatus reports whether the persistent certificate authority of the sandbox credential +// proxy exists, whether OS trust includes it, and whether it must be rotated. Changes +// nothing. +// +// RPC method: sandbox.proxyCa.getStatus. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) GetStatus(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.getStatus", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Removes the persistent certificate authority of the sandbox credential proxy from OS +// trust. Keeps the stored certificate authority. Can show an OS authentication prompt. +// Sandboxed clients that read only OS trust then reject the proxy; clients that read the +// per-process certificate bundle continue to work. +// +// RPC method: sandbox.proxyCa.remove. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Remove(ctx context.Context) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.remove", nil) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Rotate replaces the persistent certificate authority of the sandbox credential proxy with +// a new one for the current credential hosts. If OS trust included the old one, removes it +// and trusts the new one, which can show an OS authentication prompt. Running sandboxed +// tools keep the old certificate authority until they restart. +// +// RPC method: sandbox.proxyCa.rotate. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Rotate(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.rotate", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Trust adds the persistent certificate authority of the sandbox credential proxy to OS +// trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. +// Refuses a certificate authority that is not constrained to the current credential hosts. +// Can show an OS authentication prompt. +// +// RPC method: sandbox.proxyCa.trust. +// +// Parameters: Identifies the credential hosts that the persistent certificate authority of +// the sandbox credential proxy must cover. The runtime always adds the hosts from the saved +// user settings. +// +// Returns: Status of the persistent certificate authority of the sandbox credential proxy. +func (a *ServerSandboxProxyCaAPI) Trust(ctx context.Context, params *SandboxProxyCaRequest) (*SandboxProxyCaStatus, error) { + raw, err := a.client.Request(ctx, "sandbox.proxyCa.trust", params) + if err != nil { + return nil, err + } + var result SandboxProxyCaStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ProxyCa returns experimental APIs that may change or be removed. +func (s *ServerSandboxAPI) ProxyCa() *ServerSandboxProxyCaAPI { + return (*ServerSandboxProxyCaAPI)(s) +} + // Experimental: ServerSecretsAPI contains experimental APIs that may change or be removed. type ServerSecretsAPI serverAPI @@ -28127,17 +29749,15 @@ type ServerUserAPI serverAPI // removed. type ServerUserSettingsAPI serverAPI -// Get lists every known user setting (settings.json overlaid with the legacy config.json, -// config.json wins), each with its effective value, its default, and whether it is at the -// default — so settings the user has never set still appear with their default value. Does -// not include repository- or enterprise-managed overrides that the runtime layers on top at -// session time. +// Get lists every known user setting from settings.json, each with its effective value, its +// default, and whether it is at the default — so settings the user has never set still +// appear with their default value. Does not include repository- or enterprise-managed +// overrides that the runtime layers on top at session time. // // RPC method: user.settings.get. // -// Returns: Per-key metadata for every known user setting (settings.json overlaid with the -// legacy config.json, config.json wins), including settings left at their default. Excludes -// repository- and enterprise-managed overrides. +// Returns: Per-key metadata for every known user setting in settings.json, including +// settings left at their default. Excludes repository- and enterprise-managed overrides. func (a *ServerUserSettingsAPI) Get(ctx context.Context) (*UserSettingsGetResult, error) { raw, err := a.client.Request(ctx, "user.settings.get", nil) if err != nil { @@ -28150,33 +29770,13 @@ func (a *ServerUserSettingsAPI) Get(ctx context.Context) (*UserSettingsGetResult return &result, nil } -// Reload drops this runtime process's in-memory user settings cache so the next settings -// read observes disk. -// -// RPC method: user.settings.reload. -func (a *ServerUserSettingsAPI) Reload(ctx context.Context) (*UserSettingsReloadResult, error) { - raw, err := a.client.Request(ctx, "user.settings.reload", nil) - if err != nil { - return nil, err - } - var result UserSettingsReloadResult - if err := json.Unmarshal(raw, &result); err != nil { - return nil, err - } - return &result, nil -} - // Set writes one or more user settings to settings.json, replacing each provided top-level -// key. A key whose value is null is removed. Returns the keys whose new value is shadowed -// by a legacy config.json entry (config.json wins on read), which the runtime leaves in -// place — such writes do not take effect until the legacy value is removed. +// key. A key whose value is null is removed. // // RPC method: user.settings.set. // // Parameters: Partial user settings to write to settings.json. Each top-level key is // written individually, replacing the existing value; a key whose value is null is removed. -// -// Returns: Outcome of writing user settings. func (a *ServerUserSettingsAPI) Set(ctx context.Context, params *UserSettingsSetRequest) (*UserSettingsSetResult, error) { raw, err := a.client.Request(ctx, "user.settings.set", params) if err != nil { @@ -28204,6 +29804,7 @@ type ServerRPC struct { Agents *ServerAgentsAPI Catalog *ServerCatalogAPI Commands *ServerCommandsAPI + Connectors *ServerConnectorsAPI Environments *ServerEnvironmentsAPI Extensions *ServerExtensionsAPI Hooks *ServerHooksAPI @@ -28272,6 +29873,7 @@ func NewServerRPC(client *jsonrpc2.Client) *ServerRPC { r.Agents = (*ServerAgentsAPI)(&r.common) r.Catalog = (*ServerCatalogAPI)(&r.common) r.Commands = (*ServerCommandsAPI)(&r.common) + r.Connectors = (*ServerConnectorsAPI)(&r.common) r.Environments = (*ServerEnvironmentsAPI)(&r.common) r.Extensions = (*ServerExtensionsAPI)(&r.common) r.Hooks = (*ServerHooksAPI)(&r.common) @@ -28297,33 +29899,440 @@ type internalServerAPI struct { client *jsonrpc2.Client } -// Experimental: InternalServerAccountsAPI contains experimental APIs that may change or be +// Experimental: InternalServerAgentsAPI contains experimental APIs that may change or be // removed. -type InternalServerAccountsAPI internalServerAPI +type InternalServerAgentsAPI internalServerAPI -// AcquireEntraToken acquire a Microsoft Entra access token through the runtime's OneAuth -// broker. Account-scoped because it uses the same native broker as the account stack: a -// trusted host application mints a scoped Entra token for its own use, most notably to -// authenticate to a remote MCP server whose authorization server is Entra ID (in place of -// the generic browser-OAuth flow). +// CustomAgentInitialModelDecision resolves the model a custom agent asks for against the +// models actually available, and answers both the model to switch to and the warning a user +// should see when the agent's preference cannot be met. A custom agent may name several +// acceptable models in preference order, so the decision is a match rather than a lookup, +// and an agent whose preference is unavailable is a normal outcome that produces a warning +// rather than an error. A host must call this rather than pick the first available name +// itself, because the preference order and the wording of the warning are what keep one +// installation's agent selection the same as another's. // -// RPC method: accounts.acquireEntraToken. +// RPC method: agents.customAgentInitialModelDecision. // -// Parameters: OneAuth token request supplied by a trusted host application. +// Parameters: The models a custom agent asks for, and the models actually available. // -// Returns: Result of a OneAuth token acquisition. -// Internal: AcquireEntraToken is part of the SDK's internal handshake/plumbing; external +// Returns: The model to switch to, and the warning to show when the agent's preference +// could not be met. +// Internal: CustomAgentInitialModelDecision is part of the SDK's internal +// handshake/plumbing; external callers should not use it. +func (a *InternalServerAgentsAPI) CustomAgentInitialModelDecision(ctx context.Context, params *AgentsCustomAgentInitialModelDecisionParams) (*AgentsCustomAgentInitialModelDecisionResult, error) { + raw, err := a.client.Request(ctx, "agents.customAgentInitialModelDecision", params) + if err != nil { + return nil, err + } + var result AgentsCustomAgentInitialModelDecisionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetAvailableBuiltins lists the shipped agents a client should offer right now, filtered +// by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; +// some of those are gated, so a client rendering a picker wants this narrower list together +// with the description to show beside each name. +// +// RPC method: agents.getAvailableBuiltins. +// +// Parameters: The feature flags to evaluate shipped agents against. +// +// Returns: The shipped agents available under the requested flags. +// Internal: GetAvailableBuiltins is part of the SDK's internal handshake/plumbing; external // callers should not use it. -func (a *InternalServerAccountsAPI) AcquireEntraToken(ctx context.Context, params *EntraTokenAcquireRequest) (EntraTokenAcquireResult, error) { - raw, err := a.client.Request(ctx, "accounts.acquireEntraToken", params) +func (a *InternalServerAgentsAPI) GetAvailableBuiltins(ctx context.Context, params *AgentsGetAvailableBuiltinsRequest) (*AgentsGetAvailableBuiltinsResult, error) { + raw, err := a.client.Request(ctx, "agents.getAvailableBuiltins", params) if err != nil { return nil, err } - result, err := unmarshalEntraTokenAcquireResult(raw) + var result AgentsGetAvailableBuiltinsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltinDefinition loads one shipped agent's YAML definition, for a client that needs +// what the agent declares rather than only its name. `getBuiltins` reports which names have +// a definition to load: a name outside its `yamlBasedNames` is special-cased in code and +// has none. The definition crosses as its own JSON rather than as contract-typed fields, +// because the runtime parses it with the agent schema's tolerant shape and re-typing it +// here would drop the keys that shape accepts and this one does not. The projected +// `__nativeCustomAgent` view the runtime derives is included, so a caller reading the +// declared model and a caller rendering the agent see the same definition. +// +// RPC method: agents.getBuiltinDefinition. +// +// Parameters: The shipped agent whose definition to load. +// +// Returns: One shipped agent's definition. +// Internal: GetBuiltinDefinition is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerAgentsAPI) GetBuiltinDefinition(ctx context.Context, params *AgentsGetBuiltinDefinitionRequest) (*AgentsGetBuiltinDefinitionResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltinDefinition", params) if err != nil { return nil, err } - return result, nil + var result AgentsGetBuiltinDefinitionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltinListingDefinition projects one shipped agent the way a picker lists it, reading +// only the metadata at the head of the definition file and stopping before the prompt body. +// `getBuiltinDefinition` answers the whole definition instead, so a client listing every +// shipped agent should prefer this one: the cost of a listing grows with the number of +// agents, and the prompt body is the part a listing never shows. The two also differ in +// shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` +// returns the authored definition with that projection nested under `__nativeCustomAgent`. +// +// RPC method: agents.getBuiltinListingDefinition. +// +// Parameters: The shipped agent whose listing entry to load. +// +// Returns: One shipped agent, projected for a listing. +// Internal: GetBuiltinListingDefinition is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerAgentsAPI) GetBuiltinListingDefinition(ctx context.Context, params *AgentsGetBuiltinListingDefinitionRequest) (*AgentsGetBuiltinListingDefinitionResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltinListingDefinition", params) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinListingDefinitionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBuiltins lists the agents this runtime ships, by name. A consumer separating shipped +// agents from ones the user or a plugin authored should compare against these names rather +// than against `AgentInfo.source`: an authored agent may carry the `builtin` source while +// not being one of these, and the runtime treats the two as separate questions. +// `disableableNames` is the subset a user may turn off, which a client needs to decide +// whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML +// definition, which a client needs before asking the runtime to load one. +// +// RPC method: agents.getBuiltins. +// +// Returns: The agents this runtime ships, named so a consumer can tell them apart from +// authored ones. +// Internal: GetBuiltins is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerAgentsAPI) GetBuiltins(ctx context.Context) (*AgentsGetBuiltinsResult, error) { + raw, err := a.client.Request(ctx, "agents.getBuiltins", nil) + if err != nil { + return nil, err + } + var result AgentsGetBuiltinsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitAPI contains experimental APIs that may change or be +// removed. +type InternalServerGitAPI internalServerAPI + +// CurrentBranchRemote reads the remote that the branch checked out in a working tree +// tracks, as `branch..remote` configures it. Reports `origin` rather than failing +// whenever there is no tracking configuration to read — on a detached HEAD, on a branch +// with no upstream, or when git itself fails — because a caller asking which remote to talk +// to needs an answer it can act on, not an error. Marked internal because it exists to +// carry a CLI call site off the napi boundary onto the SDK contract; it is migration +// plumbing, not a surface consumers are meant to depend on. +// +// RPC method: git.currentBranchRemote. +// +// Parameters: Working-tree path a git query applies to. +// +// Returns: The remote the checked-out branch tracks. +// Internal: CurrentBranchRemote is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitAPI) CurrentBranchRemote(ctx context.Context, params *GitCwdRequest) (*GitCurrentBranchRemoteResult, error) { + raw, err := a.client.Request(ctx, "git.currentBranchRemote", params) + if err != nil { + return nil, err + } + var result GitCurrentBranchRemoteResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ReposFromRemotes lists the GitHub repositories a working tree's remotes point at, one +// entry per distinct repository, so a caller can resolve a base and head repository without +// parsing remote URLs itself. When several remotes name the same repository, only the first +// is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are +// left out, so an empty list means the tree reaches GitHub through no remote. Failing to +// read the remotes is reported as an error rather than as an empty list, because the two +// mean different things to a caller. Marked internal because it exists to carry a CLI call +// site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface +// consumers are meant to depend on. +// +// RPC method: git.reposFromRemotes. +// +// Parameters: Git working tree whose GitHub remotes should be listed. +// +// Returns: The GitHub repositories a working tree's remotes point at. +// Internal: ReposFromRemotes is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitAPI) ReposFromRemotes(ctx context.Context, params *GitReposFromRemotesRequest) (*GitReposFromRemotesResult, error) { + raw, err := a.client.Request(ctx, "git.reposFromRemotes", params) + if err != nil { + return nil, err + } + var result GitReposFromRemotesResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// WorkingDirectoryContext collects the repository context of a working directory in one +// call: working tree root, repository identifier and host, current branch, and the HEAD and +// base commits. Every repository field is omitted when the path is not inside a git working +// tree, and the requested path is echoed back as `cwd`. The answer is the same +// `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so +// a caller polling for a context change can forward the result unchanged. Marked internal +// because it exists to carry a CLI call site off the napi boundary onto the SDK contract; +// it is migration plumbing, not a surface consumers are meant to depend on. It can become +// public once an SDK consumer needs to derive session context from a directory itself. +// +// RPC method: git.workingDirectoryContext. +// +// Parameters: Working-tree path a git query applies to. +// +// Returns: Updated working directory and git context. Emitted as the new payload of +// `session.context_changed`. +// Internal: WorkingDirectoryContext is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerGitAPI) WorkingDirectoryContext(ctx context.Context, params *GitCwdRequest) (*SessionWorkingDirectoryContext, error) { + raw, err := a.client.Request(ctx, "git.workingDirectoryContext", params) + if err != nil { + return nil, err + } + var result SessionWorkingDirectoryContext + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitHubOwnersAPI contains experimental APIs that may change or +// be removed. +type InternalServerGitHubOwnersAPI internalServerAPI + +// Cancel abandons an owner listing started with the given request id. Answers `canceled: +// true` while a listing with that id is running. Answers `canceled: false` when the id was +// never registered, was registered but not used, was released after being abandoned, or its +// listing has ended. Canceling an unused id releases it, and a later `list` with that id is +// refused. The cancel acts only on owner listings and never reaches another request of the +// host. +// +// RPC method: gitHubOwners.cancel. +// +// Parameters: The owner listing to abandon. +// +// Returns: Whether the id named a running owner listing. +// Internal: Cancel is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGitHubOwnersAPI) Cancel(ctx context.Context, params *GitHubOwnersCancelRequest) (*GitHubOwnersCancelResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.cancel", params) + if err != nil { + return nil, err + } + var result GitHubOwnersCancelResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists the logins the authenticated user may act as — their own account first, then the +// organizations they belong to — by asking the GitHub API under the supplied credential. No +// credential travels in the request: `authInfo` selects one the runtime already holds, and +// the runtime resolves the token and the GitHub host from it. A failure the caller should +// render arrives as `message`; one it should raise arrives as `throwError`. +// +// RPC method: gitHubOwners.list. +// +// Parameters: Credential to list owners under, and the request id that makes the listing +// cancellable. +// +// Returns: Outcome of an owner listing. Exactly one of `owners` and `message` is present, +// except that `throwError` reports a failure the caller is expected to raise rather than +// render. +// Internal: List is part of the SDK's internal handshake/plumbing; external callers should +// not use it. +func (a *InternalServerGitHubOwnersAPI) List(ctx context.Context, params *GitHubOwnersListRequest) (*GitHubOwnersListResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.list", params) + if err != nil { + return nil, err + } + var result GitHubOwnersListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// NextRequestId registers a cancellable owner listing and returns its request id. Separate +// from `gitHubOwners.list` so the id exists before the listing starts: a caller that +// abandons the listing the moment it begins would otherwise have nothing to name in +// `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be +// released by later allocations. +// +// RPC method: gitHubOwners.nextRequestId. +// +// Returns: A freshly registered request id. Registering it before the listing starts is +// what lets a cancel that races the request still find the owner listing slot. The id +// serves one listing only. Long-abandoned unused ids can be released by later allocations. +// Internal: NextRequestId is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGitHubOwnersAPI) NextRequestId(ctx context.Context) (*GitHubOwnersRequestIDResult, error) { + raw, err := a.client.Request(ctx, "gitHubOwners.nextRequestId", nil) + if err != nil { + return nil, err + } + var result GitHubOwnersRequestIDResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGitHubRepositoryAPI contains experimental APIs that may +// change or be removed. +type InternalServerGitHubRepositoryAPI internalServerAPI + +// AtPath resolves the GitHub repository that owns a working-tree path by reading the +// selected git remote configured for it, preferring `origin`. Returns a null `repository` +// when the path is inside a git working tree but that selected remote does not resolve to a +// GitHub host. Fails when the path is not inside a git working tree at all, so a caller can +// tell 'not a repository' apart from 'a repository with no GitHub remote'. +// +// RPC method: gitHubRepository.atPath. +// +// Parameters: Working-tree path whose owning GitHub repository should be resolved. +// +// Returns: The GitHub repository that owns the requested path, when the selected remote +// (`origin`, else the first) is on a GitHub host. +// Internal: AtPath is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGitHubRepositoryAPI) AtPath(ctx context.Context, params *GitHubRepositoryAtPathRequest) (*GitHubRepositoryAtPathResult, error) { + raw, err := a.client.Request(ctx, "gitHubRepository.atPath", params) + if err != nil { + return nil, err + } + var result GitHubRepositoryAtPathResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: InternalServerGlobalStateAPI contains experimental APIs that may change or +// be removed. +type InternalServerGlobalStateAPI internalServerAPI + +// Load reads the host's machine-wide state: which plugins are installed and the one-off +// flags and timestamps that record what the user has already been shown or migrated. This +// is the state that outlives a single session and a single workspace, so a host reads it to +// decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has +// already completed. The stored credentials are deliberately not part of this result; a +// caller that needs an authenticated identity asks the account methods for it instead. +// Reading is non-destructive and every field is optional, because a fresh install has +// recorded nothing yet. +// +// RPC method: globalState.load. +// +// Returns: The host's machine-wide state. Every field is optional because a fresh install +// has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Internal: Load is part of the SDK's internal handshake/plumbing; external callers should +// not use it. +func (a *InternalServerGlobalStateAPI) Load(ctx context.Context) (*GlobalStateLoadResult, error) { + raw, err := a.client.Request(ctx, "globalState.load", nil) + if err != nil { + return nil, err + } + var result GlobalStateLoadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// LoadForConfigDir reads the host's machine-wide state exactly as `globalState.load` does, +// but from a caller-supplied configuration directory instead of the one the server resolved +// for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's +// per-session `configDir` override — so the state read matches the directory that session +// actually uses. An absent or empty `configDir` resolves the server's own home, making this +// identical to `globalState.load`. The stored credentials are omitted here for the same +// reason they are omitted from `globalState.load`: a caller that needs an authenticated +// identity asks the account methods instead, so pointing this at another directory cannot +// be used to read the credentials kept in it. +// +// RPC method: globalState.loadForConfigDir. +// +// Parameters: Selects the configuration directory whose machine-wide state to read. +// +// Returns: The host's machine-wide state. Every field is optional because a fresh install +// has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a +// negative answer. Stored credentials are deliberately absent from this shape. +// Internal: LoadForConfigDir is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerGlobalStateAPI) LoadForConfigDir(ctx context.Context, params *GlobalStateLoadForConfigDirRequest) (*GlobalStateLoadResult, error) { + raw, err := a.client.Request(ctx, "globalState.loadForConfigDir", params) + if err != nil { + return nil, err + } + var result GlobalStateLoadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// WriteKey records one top-level key in the host's machine-wide state, the counterpart to +// `globalState.load`. A host calls this to remember that it has shown an onboarding step, +// asked a one-off question, or completed a migration, so the next run can skip it. Only the +// named key is replaced and the rest of the document is preserved, which lets two writers +// record different flags without overwriting each other; passing no value removes the key +// instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, +// `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, +// `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every +// other key is refused, including `installedPlugins`, the stored credentials, +// `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use +// the plugin APIs, which apply repository and managed-policy checks. +// +// RPC method: globalState.writeKey. +// +// Parameters: A single top-level key to record in the host's machine-wide state. The write +// replaces only that key and leaves the rest of the document untouched, so two writers +// recording different one-off flags do not overwrite each other. The stored credential keys +// cannot be written through this method. +// Internal: WriteKey is part of the SDK's internal handshake/plumbing; external callers +// should not use it. +func (a *InternalServerGlobalStateAPI) WriteKey(ctx context.Context, params *GlobalStateWriteKeyRequest) (*GlobalStateWriteKeyResult, error) { + raw, err := a.client.Request(ctx, "globalState.writeKey", params) + if err != nil { + return nil, err + } + var result GlobalStateWriteKeyResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil } // Experimental: InternalServerHostAPI contains experimental APIs that may change or be @@ -28464,6 +30473,33 @@ func (a *InternalServerSessionsAPI) ConfigureSessionExtensions(ctx context.Conte return &result, nil } +// CreateWorkspace creates the workspace record for a session that has not been opened yet. +// A host that hands a session off to another application — writing the record and then +// launching that application against the session ID — needs the record on disk before any +// session exists to carry it, which the session-scoped workspace methods cannot do. +// Replaces any existing record and resets the checkpoint index. When writing to the local +// filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a +// surviving stored `fork_count` can differ from the answer. +// +// RPC method: sessions.createWorkspace. +// +// Parameters: Identity, state location and starting context for a workspace record. +// +// Returns: The workspace record that was written. +// Internal: CreateWorkspace is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerSessionsAPI) CreateWorkspace(ctx context.Context, params *SessionsCreateWorkspaceRequest) (*SessionsCreateWorkspaceResult, error) { + raw, err := a.client.Request(ctx, "sessions.createWorkspace", params) + if err != nil { + return nil, err + } + var result SessionsCreateWorkspaceResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Deletes one local session from disk after running the same lifecycle hooks as the session // manager. // @@ -28604,15 +30640,73 @@ func (a *InternalServerSessionsAPI) ListNonEmptySessionIds(ctx context.Context, return &result, nil } +// LoadWorkspace reads a session's workspace record straight from disk, without opening the +// session. Resuming by session ID has to know where the session lives before it can +// connect, so the lookup cannot come from the session-scoped workspace methods, which +// resolve their location from a live session's context. Returns no record when the file is +// absent. +// +// RPC method: sessions.loadWorkspace. +// +// Parameters: Where the session's state lives, as a root directory and the session ID under +// it. +// +// Returns: The workspace record on disk, omitted when the session has none. +// Internal: LoadWorkspace is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalServerSessionsAPI) LoadWorkspace(ctx context.Context, params *SessionsLoadWorkspaceRequest) (*SessionsLoadWorkspaceResult, error) { + raw, err := a.client.Request(ctx, "sessions.loadWorkspace", params) + if err != nil { + return nil, err + } + var result SessionsLoadWorkspaceResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// UpdateWorkspaceFields merges fields into a session's workspace record on disk, creating +// the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same +// before-the-session-exists case. It preserves stored workspace-schema fields the request +// does not supply, does not preserve stored keys outside the workspace schema, and never +// replaces a stored `fork_count`. +// +// RPC method: sessions.updateWorkspaceFields. +// +// Parameters: Where the session's state lives, plus workspace-schema fields to merge into +// its workspace record. Stored keys outside the schema are not preserved, and a stored +// `fork_count` is never replaced. +// +// Returns: The merge completed. The record carries the supplied workspace-schema fields, +// but a stored `fork_count` stays. +// Internal: UpdateWorkspaceFields is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalServerSessionsAPI) UpdateWorkspaceFields(ctx context.Context, params *SessionsUpdateWorkspaceFieldsRequest) (*SessionsUpdateWorkspaceFieldsResult, error) { + raw, err := a.client.Request(ctx, "sessions.updateWorkspaceFields", params) + if err != nil { + return nil, err + } + var result SessionsUpdateWorkspaceFieldsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // InternalServerRPC provides internal SDK server-scoped RPC methods (handshake helpers // etc.). Not part of the public API. type InternalServerRPC struct { // Reuse a single struct instead of allocating one for each service on the heap. common internalServerAPI - Accounts *InternalServerAccountsAPI - Host *InternalServerHostAPI - Sessions *InternalServerSessionsAPI + Agents *InternalServerAgentsAPI + Git *InternalServerGitAPI + GitHubOwners *InternalServerGitHubOwnersAPI + GitHubRepository *InternalServerGitHubRepositoryAPI + GlobalState *InternalServerGlobalStateAPI + Host *InternalServerHostAPI + Sessions *InternalServerSessionsAPI } // Connect performs the SDK server connection handshake and validates the optional @@ -28647,7 +30741,11 @@ func (a *InternalServerRPC) Connect(ctx context.Context, params *ConnectRequest) func NewInternalServerRPC(client *jsonrpc2.Client) *InternalServerRPC { r := &InternalServerRPC{} r.common = internalServerAPI{client: client} - r.Accounts = (*InternalServerAccountsAPI)(&r.common) + r.Agents = (*InternalServerAgentsAPI)(&r.common) + r.Git = (*InternalServerGitAPI)(&r.common) + r.GitHubOwners = (*InternalServerGitHubOwnersAPI)(&r.common) + r.GitHubRepository = (*InternalServerGitHubRepositoryAPI)(&r.common) + r.GlobalState = (*InternalServerGlobalStateAPI)(&r.common) r.Host = (*InternalServerHostAPI)(&r.common) r.Sessions = (*InternalServerSessionsAPI)(&r.common) return r @@ -29406,6 +31504,24 @@ func (a *ConnectorsAPI) Disconnect(ctx context.Context, params *ConnectorConnect return &result, nil } +// GetAccount returns the session account selection, or null. +// +// RPC method: session.connectors.getAccount. +// +// Returns: Session account selection, or null. +func (a *ConnectorsAPI) GetAccount(ctx context.Context) (*ConnectorSessionAccount, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.connectors.getAccount", req) + if err != nil { + return nil, err + } + var result *ConnectorSessionAccount + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return result, nil +} + // GetCapabilities returns feature availability and bounded polling limits for the // EXPERIMENTAL session connector API. This method never performs a Connector service // request. @@ -29486,6 +31602,9 @@ func (a *ConnectorsAPI) Reconcile(ctx context.Context, params *ConnectorReconcil req := map[string]any{"sessionId": a.sessionID} if params != nil { req["accountId"] = params.AccountID + if params.ForceConnectorName != nil { + req["forceConnectorName"] = *params.ForceConnectorName + } if params.RefreshCatalog != nil { req["refreshCatalog"] = *params.RefreshCatalog } @@ -29586,21 +31705,24 @@ func (a *ContentExclusionAPI) CheckPaths(ctx context.Context, params *ContentExc // Experimental: CustomizationsAPI contains experimental APIs that may change or be removed. type CustomizationsAPI sessionAPI -// Reloads all repository and user customizations for the active session: instructions, -// plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns -// diagnostics from the final skill reload. +// Reload for local sessions, reconciles repository context and discovered instructions, +// plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change +// under the working directory. Independent component failures are returned in outcomes and +// errors; a rejected call can have partially applied earlier steps. Remote sessions must +// reload on their agent host instead. The model-facing context is rebuilt on the next turn. // // RPC method: session.customizations.reload. // -// Returns: Diagnostics from reloading skill definitions, with warnings and errors as -// separate lists. -func (a *CustomizationsAPI) Reload(ctx context.Context) (*SkillsLoadDiagnostics, error) { +// Returns: Results of reloading discovered session customizations. Inspect outcomes for +// reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes +// to the model-facing prompt and tools apply on the next turn. +func (a *CustomizationsAPI) Reload(ctx context.Context) (*CustomizationsReloadResult, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.customizations.reload", req) if err != nil { return nil, err } - var result SkillsLoadDiagnostics + var result CustomizationsReloadResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } @@ -30261,17 +32383,20 @@ func (a *InstructionsAPI) GetSources(ctx context.Context) (*InstructionsGetSourc return &result, nil } -// Reload invalidates cached custom-instruction discovery so subsequent turns and source -// reads observe instruction files currently on disk. +// Reload for local sessions, invalidates instruction discovery and the model-facing prompt, +// then returns freshly discovered sources. The updated prompt takes effect on the next +// turn. Remote sessions must reload on their agent host instead. // // RPC method: session.instructions.reload. -func (a *InstructionsAPI) Reload(ctx context.Context) (*SessionInstructionsReloadResult, error) { +// +// Returns: Instruction sources loaded for the session, in merge order. +func (a *InstructionsAPI) Reload(ctx context.Context) (*InstructionsGetSourcesResult, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.instructions.reload", req) if err != nil { return nil, err } - var result SessionInstructionsReloadResult + var result InstructionsGetSourcesResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } @@ -30504,10 +32629,8 @@ func (a *MCPAPI) IsServerRunning(ctx context.Context, params *MCPIsServerRunning return &result, nil } -// Lists MCP servers configured for the session, their connection status, and host-level -// state. The host-level state (disabled/filtered servers, failed/needs-auth/pending -// connections, mcp3p policy, full config) is empty/zero when no MCP host has been -// initialized for the session. +// Lists materialized MCP servers and their connection status. Cache misses may start and +// wait for MCP servers. // // RPC method: session.mcp.list. // @@ -30526,6 +32649,27 @@ func (a *MCPAPI) List(ctx context.Context) (*MCPServerList, error) { return &result, nil } +// ListConfigured lists effective MCP configuration without starting, restarting, +// authenticating, or waiting for servers. An optional live observation is from an already +// materialized matching server; this is not a readiness guarantee. +// +// RPC method: session.mcp.listConfigured. +// +// Returns: Effective MCP configuration with optional live observations from matching +// already materialized servers. +func (a *MCPAPI) ListConfigured(ctx context.Context) (*MCPConfiguredServerList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.mcp.listConfigured", req) + if err != nil { + return nil, err + } + var result MCPConfiguredServerList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // ListTools lists the tools exposed by a connected MCP server on this session's host. This // performs a live `tools/list` request. Tool UI metadata is returned independently of // whether MCP Apps rendering is enabled for the session. @@ -30968,6 +33112,29 @@ func (a *MCPOauthAPI) CancelLogin(ctx context.Context, params *SessionMCPOauthCa return &result, nil } +// Completes a runtime-managed MCP OAuth login after the authorization server redirects to a +// host-managed callback URL. +// +// RPC method: session.mcp.oauth.complete. +// +// Parameters: Host-delivered callback for a runtime-managed MCP OAuth login. +func (a *MCPOauthAPI) Complete(ctx context.Context, params *MCPOauthCompleteRequest) (*SessionMCPOauthCompleteResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["authorizationId"] = params.AuthorizationID + req["callbackUrl"] = params.CallbackURL + } + raw, err := a.client.Request(ctx, "session.mcp.oauth.complete", req) + if err != nil { + return nil, err + } + var result SessionMCPOauthCompleteResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // HandlePendingRequest resolves a pending MCP OAuth request with a host-provided token or // cancellation. The pending request is emitted as mcp.oauth_required with the data // necessary to authorize the request. @@ -31001,7 +33168,7 @@ func (a *MCPOauthAPI) HandlePendingRequest(ctx context.Context, params *MCPOauth // RPC method: session.mcp.oauth.login. // // Parameters: Remote MCP server name and optional overrides controlling reauthentication, -// OAuth client display name, callback success-page copy, and static OAuth client selection. +// OAuth client display name, callback handling, and static OAuth client selection. // // Returns: OAuth authorization URL the caller should open, or empty when cached tokens // already authenticated the server. @@ -31035,6 +33202,9 @@ func (a *MCPOauthAPI) Login(ctx context.Context, params *MCPOauthLoginRequest) ( if params.PublicClient != nil { req["publicClient"] = *params.PublicClient } + if params.RedirectURI != nil { + req["redirectUri"] = *params.RedirectURI + } req["serverName"] = params.ServerName } raw, err := a.client.Request(ctx, "session.mcp.oauth.login", req) @@ -31145,6 +33315,70 @@ func (s *MCPAPI) Oauth() *MCPOauthAPI { return (*MCPOauthAPI)(s) } +// Experimental: MCPPromptsAPI contains experimental APIs that may change or be removed. +type MCPPromptsAPI sessionAPI + +// Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content +// is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch +// referenced resources. +// +// RPC method: session.mcp.prompts.get. +// +// Parameters: MCP server, prompt name, and optional string-valued arguments. +// +// Returns: Prompt messages returned by the MCP server without sending them to the model. +func (a *MCPPromptsAPI) Get(ctx context.Context, params *MCPPromptsGetRequest) (*MCPPromptsGetResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Arguments != nil { + req["arguments"] = params.Arguments + } + req["promptName"] = params.PromptName + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.prompts.get", req) + if err != nil { + return nil, err + } + var result MCPPromptsGetResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// List enumerate one page of prompts a connected MCP server exposes (proxies MCP +// `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. +// +// RPC method: session.mcp.prompts.list. +// +// Parameters: MCP server whose prompts to enumerate. +// +// Returns: One page of prompts advertised by the named MCP server. +func (a *MCPPromptsAPI) List(ctx context.Context, params *MCPPromptsListRequest) (*MCPPromptsListResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Cursor != nil { + req["cursor"] = *params.Cursor + } + req["serverName"] = params.ServerName + } + raw, err := a.client.Request(ctx, "session.mcp.prompts.list", req) + if err != nil { + return nil, err + } + var result MCPPromptsListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Prompts returns experimental APIs that may change or be removed. +func (s *MCPAPI) Prompts() *MCPPromptsAPI { + return (*MCPPromptsAPI)(s) +} + // Experimental: MCPResourcesAPI contains experimental APIs that may change or be removed. type MCPResourcesAPI sessionAPI @@ -33221,6 +35455,146 @@ func (a *ProviderAPI) Withdraw(ctx context.Context, params *ProviderWithdrawRequ return &result, nil } +// Experimental: ProvidersAPI contains experimental APIs that may change or be removed. +type ProvidersAPI sessionAPI + +// Discovers reachable instances using an adapter from this session's effective provider +// catalog and provider-specific discovery input. +// +// RPC method: session.providers.discover. +// +// Parameters: Provider discovery parameters. +// +// Returns: Provider instances found by a discovery operation. +func (a *ProvidersAPI) Discover(ctx context.Context, params *ModelProviderDiscoverRequest) (*ModelProviderDiscoverResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["adapterId"] = params.AdapterID + if params.Input != nil { + req["input"] = params.Input + } + } + raw, err := a.client.Request(ctx, "session.providers.discover", req) + if err != nil { + return nil, err + } + var result ModelProviderDiscoverResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetCatalog returns adapter definitions and supported operations in this session's +// effective provider catalog, without running discovery. Does not list provider instances +// or select inference models. +// +// RPC method: session.providers.getCatalog. +// +// Returns: Normalized model-provider adapter definitions available to the session, not +// discovered instances. +func (a *ProvidersAPI) GetCatalog(ctx context.Context) (*ModelProviderAdapterCatalog, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.providers.getCatalog", req) + if err != nil { + return nil, err + } + var result ModelProviderAdapterCatalog + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetStatus gets current health and version information for a discovered model-provider +// instance. +// +// RPC method: session.providers.getStatus. +// +// Parameters: Provider status request parameters. +// +// Returns: Current health information for a provider instance. +func (a *ProvidersAPI) GetStatus(ctx context.Context, params *ModelProviderGetStatusRequest) (*ModelProviderStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + } + raw, err := a.client.Request(ctx, "session.providers.getStatus", req) + if err != nil { + return nil, err + } + var result ModelProviderStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ProvidersModelsAPI contains experimental APIs that may change or be removed. +type ProvidersModelsAPI sessionAPI + +// Lists models installed or otherwise available from a discovered model-provider instance. +// +// RPC method: session.providers.models.list. +// +// Parameters: Provider model inventory request parameters. +// +// Returns: Models offered for agent conversations by one provider instance. Adapters +// exclude known-incompatible models, but retain candidates with unknown capabilities. +// Listing does not guarantee compatibility. +func (a *ProvidersModelsAPI) List(ctx context.Context, params *ModelProviderModelsListRequest) (*DiscoveredModelList, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + } + raw, err := a.client.Request(ctx, "session.providers.models.list", req) + if err != nil { + return nil, err + } + var result DiscoveredModelList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// PrepareConfiguration translates a discovered model into the provider and model +// configuration needed to use it, and reports whether each is already registered in this +// session. Prepares only: it registers nothing, writes nothing, and performs no provider +// requests. +// +// RPC method: session.providers.models.prepareConfiguration. +// +// Parameters: A discovered instance and one of its models to translate into provider +// configuration. Pass back the instance and model as returned by +// `session.providers.discover` and `session.providers.models.list`. +// +// Returns: Provider configuration prepared from a discovered model. Preparing a plan +// changes nothing: it neither registers the model with the session nor writes durable +// configuration. To apply it, pass `provider` and `model` to `session.provider.add`, +// omitting whichever the dispositions report as already configured. +func (a *ProvidersModelsAPI) PrepareConfiguration(ctx context.Context, params *ModelProviderPrepareConfigurationRequest) (*ModelProviderConfigurationPlan, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instance"] = params.Instance + req["model"] = params.Model + } + raw, err := a.client.Request(ctx, "session.providers.models.prepareConfiguration", req) + if err != nil { + return nil, err + } + var result ModelProviderConfigurationPlan + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Models returns experimental APIs that may change or be removed. +func (s *ProvidersAPI) Models() *ProvidersModelsAPI { + return (*ProvidersModelsAPI)(s) +} + // Experimental: QueueAPI contains experimental APIs that may change or be removed. type QueueAPI sessionAPI @@ -33735,7 +36109,7 @@ func (a *ShellAPI) CancelUserRequested(ctx context.Context, params *ShellCancelU return &result, nil } -// Exec starts a shell command and streams output through session notifications. The command +// Exec starts a shell command, returning an RPC error if it cannot be spawned. The command // runs as the leader of its own process group (POSIX) or in a dedicated job object // (Windows), so a forced termination — via "shell.kill", the request timeout, or session // disposal — signals that whole group/job rather than only the direct child. Two gaps are @@ -33747,10 +36121,10 @@ func (a *ShellAPI) CancelUserRequested(ctx context.Context, params *ShellCancelU // RPC method: session.shell.exec. // // Parameters: Shell command to run, with optional working directory and timeout in -// milliseconds. +// milliseconds. Spawn failures return an RPC error. // -// Returns: Identifier of the spawned process, used to correlate streamed output and exit -// notifications. +// Returns: Identifier of the spawned shell process, usable with shell.kill while the +// process is running. func (a *ShellAPI) Exec(ctx context.Context, params *ShellExecRequest) (*ShellExecResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { @@ -35703,6 +38077,7 @@ type SessionRPC struct { Plan *PlanAPI Plugins *PluginsAPI Provider *ProviderAPI + Providers *ProvidersAPI Queue *QueueAPI Remote *RemoteAPI Sandbox *SandboxAPI @@ -36040,6 +38415,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Plan = (*PlanAPI)(&r.common) r.Plugins = (*PluginsAPI)(&r.common) r.Provider = (*ProviderAPI)(&r.common) + r.Providers = (*ProvidersAPI)(&r.common) r.Queue = (*QueueAPI)(&r.common) r.Remote = (*RemoteAPI)(&r.common) r.Sandbox = (*SandboxAPI)(&r.common) @@ -36247,11 +38623,11 @@ func (a *InternalGitHubAuthAPI) GetCurrentAuthInfo(ctx context.Context) (*AuthId if err != nil { return nil, err } - var result AuthIdentity + var result *AuthIdentity if err := json.Unmarshal(raw, &result); err != nil { return nil, err } - return &result, nil + return result, nil } // LastAuthErrors gets validation errors from the most recent authentication attempt. @@ -36363,11 +38739,11 @@ func (a *InternalGitHubAuthAPI) RefreshCopilotUser(ctx context.Context) (*AuthId if err != nil { return nil, err } - var result AuthIdentity + var result *AuthIdentity if err := json.Unmarshal(raw, &result); err != nil { return nil, err } - return &result, nil + return result, nil } // SwitchToAuth switches the session to another available authentication. @@ -36482,6 +38858,35 @@ func (a *InternalMCPAPI) ReloadWithConfig(ctx context.Context, params *MCPReload return &result, nil } +// SetConnectedIdeInfo records the IDE the host is connected to, so the agent's system +// prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded +// value, which is how a host reports that it is disconnected; there is no separate clear +// method. Both `ideName` and `workspaceFolder` are required together, because half a state +// cannot be attributed to a project. +// +// RPC method: session.mcp.setConnectedIdeInfo. +// +// Parameters: Records which IDE the host is connected to, or clears it. +// Internal: SetConnectedIdeInfo is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalMCPAPI) SetConnectedIdeInfo(ctx context.Context, params *SessionMCPSetConnectedIdeInfoParams) (*SessionMCPSetConnectedIdeInfoResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Ide != nil { + req["ide"] = *params.Ide + } + } + raw, err := a.client.Request(ctx, "session.mcp.setConnectedIdeInfo", req) + if err != nil { + return nil, err + } + var result SessionMCPSetConnectedIdeInfoResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // UnregisterExternalClient unregisters a previously registered external MCP client by // server name. Marked internal as the paired companion of `registerExternalClient`: only // in-process callers that registered a client this way can meaningfully unregister it. @@ -37014,6 +39419,94 @@ func (a *InternalSettingsAPI) Snapshot(ctx context.Context) (*SessionSettingsSna return &result, nil } +// Experimental: InternalUIAPI contains experimental APIs that may change or be removed. +type InternalUIAPI internalSessionAPI + +// HandleHumanAskUser resolves a pending elicitation request after direct interaction in the +// trusted in-process client. Only an accepted response to the built-in ask_user tool can +// become trusted human evidence. +// +// RPC method: session.ui.handleHumanAskUser. +// +// Parameters: Pending elicitation request ID and the user's response (accept/decline/cancel +// + form values). +// +// Returns: Indicates whether the elicitation response was accepted; false if it was already +// resolved by another client. +// Internal: HandleHumanAskUser is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalUIAPI) HandleHumanAskUser(ctx context.Context, params *UIHandlePendingElicitationRequest) (*UIElicitationResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["result"] = params.Result + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanAskUser", req) + if err != nil { + return nil, err + } + var result UIElicitationResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// HandleHumanExitPlanMode resolves a pending `exit_plan_mode.requested` event after direct +// interaction in the trusted in-process client. +// +// RPC method: session.ui.handleHumanExitPlanMode. +// +// Parameters: Request ID of a pending `exit_plan_mode.requested` event and the user's +// response. +// +// Returns: Indicates whether the pending UI request was resolved by this call. +// Internal: HandleHumanExitPlanMode is part of the SDK's internal handshake/plumbing; +// external callers should not use it. +func (a *InternalUIAPI) HandleHumanExitPlanMode(ctx context.Context, params *UIHandlePendingExitPlanModeRequest) (*UIHandlePendingResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["response"] = params.Response + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanExitPlanMode", req) + if err != nil { + return nil, err + } + var result UIHandlePendingResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// HandleHumanUserInput resolves a pending `user_input.requested` event after direct +// interaction in the trusted in-process client. +// +// RPC method: session.ui.handleHumanUserInput. +// +// Parameters: Request ID of a pending `user_input.requested` event and the user's response. +// +// Returns: Indicates whether the pending UI request was resolved by this call. +// Internal: HandleHumanUserInput is part of the SDK's internal handshake/plumbing; external +// callers should not use it. +func (a *InternalUIAPI) HandleHumanUserInput(ctx context.Context, params *UIHandlePendingUserInputRequest) (*UIHandlePendingResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["response"] = params.Response + } + raw, err := a.client.Request(ctx, "session.ui.handleHumanUserInput", req) + if err != nil { + return nil, err + } + var result UIHandlePendingResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: InternalWorkflowAPI contains experimental APIs that may change or be // removed. type InternalWorkflowAPI internalSessionAPI @@ -37122,6 +39615,7 @@ type InternalSessionRPC struct { Queue *InternalQueueAPI Schedule *InternalScheduleAPI Settings *InternalSettingsAPI + UI *InternalUIAPI Workflow *InternalWorkflowAPI } @@ -37169,6 +39663,7 @@ func NewInternalSessionRPC(client *jsonrpc2.Client, sessionID string) *InternalS r.Queue = (*InternalQueueAPI)(&r.common) r.Schedule = (*InternalScheduleAPI)(&r.common) r.Settings = (*InternalSettingsAPI)(&r.common) + r.UI = (*InternalUIAPI)(&r.common) r.Workflow = (*InternalWorkflowAPI)(&r.common) return r } @@ -37282,6 +39777,14 @@ type SessionFSHandler interface { // // Returns: File content as a UTF-8 string, or a filesystem error if the read failed. ReadFile(request *SessionFSReadFileRequest) (*SessionFSReadFileResult, error) + // ReadFileBytes reads binary file content from the client-provided session filesystem. + // + // RPC method: sessionFs.readFileBytes. + // + // Parameters: Path of the binary file to read from the client-provided session filesystem. + // + // Returns: File bytes as standard base64, or a filesystem error if the read failed. + ReadFileBytes(request *SessionFSReadFileBytesRequest) (*SessionFSReadFileBytesResult, error) // Renames or moves a path in the client-provided session filesystem. // // RPC method: sessionFs.rename. @@ -37349,6 +39852,15 @@ type SessionFSHandler interface { // // Returns: Describes a filesystem error. WriteFile(request *SessionFSWriteFileRequest) (*SessionFSError, error) + // WriteFileBytes writes binary file content to the client-provided session filesystem. + // + // RPC method: sessionFs.writeFileBytes. + // + // Parameters: File path, standard-base64-encoded bytes to write, and optional mode for the + // client-provided session filesystem. + // + // Returns: Describes a filesystem error. + WriteFileBytes(request *SessionFSWriteFileBytesRequest) (*SessionFSError, error) } // Experimental: TasksHandler contains experimental APIs that may change or be removed. @@ -37598,6 +40110,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("sessionFs.readFileBytes", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request SessionFSReadFileBytesRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.SessionFS == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No sessionFs handler registered for session: %s", request.SessionID)} + } + result, err := handlers.SessionFS.ReadFileBytes(&request) + if err != nil { + return nil, clientSessionHandlerError(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("sessionFs.rename", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request SessionFSRenameRequest if err := json.Unmarshal(params, &request); err != nil { @@ -37731,6 +40262,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("sessionFs.writeFileBytes", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request SessionFSWriteFileBytesRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.SessionFS == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No sessionFs handler registered for session: %s", request.SessionID)} + } + result, err := handlers.SessionFS.WriteFileBytes(&request) + if err != nil { + return nil, clientSessionHandlerError(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("tasks.cancel", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { var request ClientTaskCancelRequest if err := json.Unmarshal(params, &request); err != nil { diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 320292785a..4835a1bef9 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -21,6 +21,12 @@ func unmarshalAuthInfo(data []byte) (AuthInfo, error) { } switch raw.Type { + case AuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case AuthInfoTypeAPIKey: var d APIKeyAuthInfo if err := json.Unmarshal(data, &d); err != nil { @@ -85,6 +91,17 @@ func (r RawAuthInfoData) MarshalJSON() ([]byte, error) { }) } +func (r AccountAuthInfo) MarshalJSON() ([]byte, error) { + type alias AccountAuthInfo + return json.Marshal(struct { + Type AuthInfoType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r APIKeyAuthInfo) MarshalJSON() ([]byte, error) { type alias APIKeyAuthInfo return json.Marshal(struct { @@ -2573,69 +2590,6 @@ func (r UnsupportedEnqueueCommandResult) MarshalJSON() ([]byte, error) { }) } -func unmarshalEntraTokenAcquireResult(data []byte) (EntraTokenAcquireResult, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Status EntraTokenAcquireResultStatus `json:"status"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Status { - case EntraTokenAcquireResultStatusInteractionRequired: - var d EntraTokenAcquireResultInteractionRequired - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case EntraTokenAcquireResultStatusOk: - var d EntraTokenAcquireResultOk - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawEntraTokenAcquireResultData{Discriminator: raw.Status, Raw: data}, nil - } -} - -func (r RawEntraTokenAcquireResultData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } - return json.Marshal(struct { - Status EntraTokenAcquireResultStatus `json:"status"` - }{ - Status: r.Discriminator, - }) -} - -func (r EntraTokenAcquireResultInteractionRequired) MarshalJSON() ([]byte, error) { - type alias EntraTokenAcquireResultInteractionRequired - return json.Marshal(struct { - Status EntraTokenAcquireResultStatus `json:"status"` - alias - }{ - Status: r.Status(), - alias: alias(r), - }) -} - -func (r EntraTokenAcquireResultOk) MarshalJSON() ([]byte, error) { - type alias EntraTokenAcquireResultOk - return json.Marshal(struct { - Status EntraTokenAcquireResultStatus `json:"status"` - alias - }{ - Status: r.Status(), - alias: alias(r), - }) -} - func (r EventLogTypes) MarshalJSON() ([]byte, error) { if r.String != nil { return json.Marshal(r.String) @@ -3053,6 +3007,58 @@ func (r GitHubTokenAcquireResultToken) MarshalJSON() ([]byte, error) { }) } +func (r InstalledPluginSource) MarshalJSON() ([]byte, error) { + if r.InstalledPluginSourceGitHub != nil { + return json.Marshal(r.InstalledPluginSourceGitHub) + } + if r.InstalledPluginSourceLocal != nil { + return json.Marshal(r.InstalledPluginSourceLocal) + } + if r.InstalledPluginSourceURL != nil { + return json.Marshal(r.InstalledPluginSourceURL) + } + if r.String != nil { + return json.Marshal(r.String) + } + return []byte("null"), nil +} + +func (r *InstalledPluginSource) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + *r = InstalledPluginSource{} + return nil + } + { + var value InstalledPluginSourceGitHub + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{InstalledPluginSourceGitHub: &value} + return nil + } + } + { + var value InstalledPluginSourceLocal + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{InstalledPluginSourceLocal: &value} + return nil + } + } + { + var value InstalledPluginSourceURL + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{InstalledPluginSourceURL: &value} + return nil + } + } + { + var value string + if err := json.Unmarshal(data, &value); err == nil { + *r = InstalledPluginSource{String: &value} + return nil + } + } + return errors.New("data did not match any union variant for InstalledPluginSource") +} + func (r *HandlePendingToolCallRequest) UnmarshalJSON(data []byte) error { type rawHandlePendingToolCallRequest struct { Error *string `json:"error,omitempty"` @@ -3606,58 +3612,6 @@ func (r *InstallationConfirmationRequest) UnmarshalJSON(data []byte) error { return nil } -func (r InstalledPluginSource) MarshalJSON() ([]byte, error) { - if r.InstalledPluginSourceGitHub != nil { - return json.Marshal(r.InstalledPluginSourceGitHub) - } - if r.InstalledPluginSourceLocal != nil { - return json.Marshal(r.InstalledPluginSourceLocal) - } - if r.InstalledPluginSourceURL != nil { - return json.Marshal(r.InstalledPluginSourceURL) - } - if r.String != nil { - return json.Marshal(r.String) - } - return []byte("null"), nil -} - -func (r *InstalledPluginSource) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - *r = InstalledPluginSource{} - return nil - } - { - var value InstalledPluginSourceGitHub - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{InstalledPluginSourceGitHub: &value} - return nil - } - } - { - var value InstalledPluginSourceLocal - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{InstalledPluginSourceLocal: &value} - return nil - } - } - { - var value InstalledPluginSourceURL - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{InstalledPluginSourceURL: &value} - return nil - } - } - { - var value string - if err := json.Unmarshal(data, &value); err == nil { - *r = InstalledPluginSource{String: &value} - return nil - } - } - return errors.New("data did not match any union variant for InstalledPluginSource") -} - func matchesMCPSerializableServerConfigMCPServerConfigHTTP(data []byte) bool { var rawGroup0 struct { Command json.RawMessage `json:"command"` @@ -5557,6 +5511,101 @@ func (r *MCPStartServerRequest) UnmarshalJSON(data []byte) error { return nil } +func unmarshalProtocolMarkerSectionOverride(data []byte) (ProtocolMarkerSectionOverride, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Action { + case ProtocolMarkerSectionOverrideActionPreserve: + var d ProtocolMarkerSectionOverridePreserve + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case ProtocolMarkerSectionOverrideActionTransform: + var d ProtocolMarkerSectionOverrideTransform + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawProtocolMarkerSectionOverrideData{Discriminator: raw.Action, Raw: data}, nil + } +} + +func (r RawProtocolMarkerSectionOverrideData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + }{ + Action: r.Discriminator, + }) +} + +func (r ProtocolMarkerSectionOverridePreserve) MarshalJSON() ([]byte, error) { + type alias ProtocolMarkerSectionOverridePreserve + return json.Marshal(struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + alias + }{ + Action: r.Action(), + alias: alias(r), + }) +} + +func (r ProtocolMarkerSectionOverrideTransform) MarshalJSON() ([]byte, error) { + type alias ProtocolMarkerSectionOverrideTransform + return json.Marshal(struct { + Action ProtocolMarkerSectionOverrideAction `json:"action"` + alias + }{ + Action: r.Action(), + alias: alias(r), + }) +} + +func (r ProtocolSectionOverride) MarshalJSON() ([]byte, error) { + if r.ProtocolMarkerSectionOverride != nil { + return json.Marshal(r.ProtocolMarkerSectionOverride) + } + if r.ProtocolStaticSectionOverride != nil { + return json.Marshal(r.ProtocolStaticSectionOverride) + } + return []byte("null"), nil +} + +func (r *ProtocolSectionOverride) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + *r = ProtocolSectionOverride{} + return nil + } + { + value, err := unmarshalProtocolMarkerSectionOverride(data) + if err == nil { + *r = ProtocolSectionOverride{ProtocolMarkerSectionOverride: value} + return nil + } + } + { + var value ProtocolStaticSectionOverride + if err := json.Unmarshal(data, &value); err == nil { + *r = ProtocolSectionOverride{ProtocolStaticSectionOverride: &value} + return nil + } + } + return errors.New("data did not match any union variant for ProtocolSectionOverride") +} + func unmarshalPermissionDecision(data []byte) (PermissionDecision, error) { if string(data) == "null" { return nil, nil @@ -6825,101 +6874,6 @@ func (r *PermissionLocationAddToolApprovalParams) UnmarshalJSON(data []byte) err return nil } -func unmarshalProtocolMarkerSectionOverride(data []byte) (ProtocolMarkerSectionOverride, error) { - if string(data) == "null" { - return nil, nil - } - type rawUnion struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - } - var raw rawUnion - if err := json.Unmarshal(data, &raw); err != nil { - return nil, err - } - - switch raw.Action { - case ProtocolMarkerSectionOverrideActionPreserve: - var d ProtocolMarkerSectionOverridePreserve - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - case ProtocolMarkerSectionOverrideActionTransform: - var d ProtocolMarkerSectionOverrideTransform - if err := json.Unmarshal(data, &d); err != nil { - return nil, err - } - return &d, nil - default: - return &RawProtocolMarkerSectionOverrideData{Discriminator: raw.Action, Raw: data}, nil - } -} - -func (r RawProtocolMarkerSectionOverrideData) MarshalJSON() ([]byte, error) { - if r.Raw != nil { - return r.Raw, nil - } - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - }{ - Action: r.Discriminator, - }) -} - -func (r ProtocolMarkerSectionOverridePreserve) MarshalJSON() ([]byte, error) { - type alias ProtocolMarkerSectionOverridePreserve - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - alias - }{ - Action: r.Action(), - alias: alias(r), - }) -} - -func (r ProtocolMarkerSectionOverrideTransform) MarshalJSON() ([]byte, error) { - type alias ProtocolMarkerSectionOverrideTransform - return json.Marshal(struct { - Action ProtocolMarkerSectionOverrideAction `json:"action"` - alias - }{ - Action: r.Action(), - alias: alias(r), - }) -} - -func (r ProtocolSectionOverride) MarshalJSON() ([]byte, error) { - if r.ProtocolMarkerSectionOverride != nil { - return json.Marshal(r.ProtocolMarkerSectionOverride) - } - if r.ProtocolStaticSectionOverride != nil { - return json.Marshal(r.ProtocolStaticSectionOverride) - } - return []byte("null"), nil -} - -func (r *ProtocolSectionOverride) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - *r = ProtocolSectionOverride{} - return nil - } - { - value, err := unmarshalProtocolMarkerSectionOverride(data) - if err == nil { - *r = ProtocolSectionOverride{ProtocolMarkerSectionOverride: value} - return nil - } - } - { - var value ProtocolStaticSectionOverride - if err := json.Unmarshal(data, &value); err == nil { - *r = ProtocolSectionOverride{ProtocolStaticSectionOverride: &value} - return nil - } - } - return errors.New("data did not match any union variant for ProtocolSectionOverride") -} - func unmarshalPushAttachment(data []byte) (PushAttachment, error) { if string(data) == "null" { return nil, nil @@ -8203,6 +8157,12 @@ func unmarshalSettableAuthInfo(data []byte) (SettableAuthInfo, error) { } switch raw.Type { + case SettableAuthInfoTypeAccount: + var d AccountAuthInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case SettableAuthInfoTypeAPIKey: var d APIKeyAuthInfo if err := json.Unmarshal(data, &d); err != nil { diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index ae79439771..fa44772dab 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -251,6 +251,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeHumanResponseRecorded: + var d HumanResponseRecordedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeMCPAppToolCallComplete: var d MCPAppToolCallCompleteData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -887,6 +893,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeToolShellOutput: + var d ToolShellOutputData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeToolUserRequested: var d ToolUserRequestedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -2643,6 +2655,110 @@ func (r *PermissionCompletedData) UnmarshalJSON(data []byte) error { return nil } +func unmarshalHumanResponseRecordedResponse(data []byte) (HumanResponseRecordedResponse, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.ResponseKind { + case HumanResponseRecordedResponseResponseKindAskUser: + var d HumanResponseRecordedResponseAskUser + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case HumanResponseRecordedResponseResponseKindExitPlanMode: + var d HumanResponseRecordedResponseExitPlanMode + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case HumanResponseRecordedResponseResponseKindUserInput: + var d HumanResponseRecordedResponseUserInput + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawHumanResponseRecordedResponse{Discriminator: raw.ResponseKind, Raw: data}, nil + } +} + +func (r RawHumanResponseRecordedResponse) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + }{ + ResponseKind: r.Discriminator, + }) +} + +func (r HumanResponseRecordedResponseAskUser) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseAskUser + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r HumanResponseRecordedResponseExitPlanMode) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseExitPlanMode + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r HumanResponseRecordedResponseUserInput) MarshalJSON() ([]byte, error) { + type alias HumanResponseRecordedResponseUserInput + return json.Marshal(struct { + ResponseKind HumanResponseRecordedResponseResponseKind `json:"responseKind"` + alias + }{ + ResponseKind: r.ResponseKind(), + alias: alias(r), + }) +} + +func (r *HumanResponseRecordedData) UnmarshalJSON(data []byte) error { + type rawHumanResponseRecordedData struct { + Actor HumanResponseActor `json:"actor"` + RequestID string `json:"requestId"` + Response json.RawMessage `json:"response"` + ToolCallID *string `json:"toolCallId,omitempty"` + } + var raw rawHumanResponseRecordedData + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.Actor = raw.Actor + r.RequestID = raw.RequestID + if raw.Response != nil { + value, err := unmarshalHumanResponseRecordedResponse(raw.Response) + if err != nil { + return err + } + r.Response = value + } + r.ToolCallID = raw.ToolCallID + return nil +} + func (r *SessionExtensionsAttachmentsPushedData) UnmarshalJSON(data []byte) error { type rawSessionExtensionsAttachmentsPushedData struct { Attachments []json.RawMessage `json:"attachments"` diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 16de7a795f..f429e809d2 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -97,6 +97,7 @@ const ( SessionEventTypeHookEnd SessionEventType = "hook.end" SessionEventTypeHookProgress SessionEventType = "hook.progress" SessionEventTypeHookStart SessionEventType = "hook.start" + SessionEventTypeHumanResponseRecorded SessionEventType = "human_response.recorded" SessionEventTypeMCPAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" SessionEventTypeMCPHeadersRefreshCompleted SessionEventType = "mcp.headers_refresh_completed" SessionEventTypeMCPHeadersRefreshRequired SessionEventType = "mcp.headers_refresh_required" @@ -241,20 +242,22 @@ const ( SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" // Experimental: SessionEventTypeSkillInvokedRef identifies an experimental event that may // change or be removed. - SessionEventTypeSkillInvokedRef SessionEventType = "skill.invoked_ref" - SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" - SessionEventTypeSubagentConfigured SessionEventType = "subagent.configured" - SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" - SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" - SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" - SessionEventTypeSubagentStarted SessionEventType = "subagent.started" - SessionEventTypeSystemMessage SessionEventType = "system.message" - SessionEventTypeSystemNotification SessionEventType = "system.notification" - SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + SessionEventTypeSkillInvokedRef SessionEventType = "skill.invoked_ref" + SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" + SessionEventTypeSubagentConfigured SessionEventType = "subagent.configured" + SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" + SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" + SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" + SessionEventTypeSubagentStarted SessionEventType = "subagent.started" + SessionEventTypeSystemMessage SessionEventType = "system.message" + SessionEventTypeSystemNotification SessionEventType = "system.notification" + SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + // Deprecated: SessionEventTypeToolExecutionPartialResult identifies a deprecated event. SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress" SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" SessionEventTypeToolSearchActivated SessionEventType = "tool_search.activated" + SessionEventTypeToolShellOutput SessionEventType = "tool.shell_output" SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" // Experimental: SessionEventTypeUIEphemeralQuery identifies an experimental event that may // change or be removed. @@ -326,6 +329,9 @@ type PromptCacheBreakData struct { // Telemetry-safe names of tools whose definition changed since the prior call // Internal: ToolsRedefined is part of the SDK's internal API surface and is not intended for external use. ToolsRedefined []string `json:"toolsRedefined,omitzero"` + // Changed definition parts of redefined tools, as `tool:part` entries; property-level parts only for telemetry-safe tools, whose other names are hashed + // Internal: ToolsRedefinedParts is part of the SDK's internal API surface and is not intended for external use. + ToolsRedefinedParts []string `json:"toolsRedefinedParts,omitzero"` // Raw names of tools redefined since the prior call, restricted because a tool name can be user-authored // Internal: ToolsRedefinedRaw is part of the SDK's internal API surface and is not intended for external use. ToolsRedefinedRaw []string `json:"toolsRedefinedRaw,omitzero"` @@ -503,6 +509,8 @@ type SessionAutoModeResolvedData struct { RouterLatencyMs *float64 `json:"routerLatencyMs,omitempty"` // The routing method the server applied, when Auto Intent ran RoutingMethod *string `json:"routingMethod,omitempty"` + // Short human-readable sentence from the routing service explaining why this model was chosen, for display alongside the model. Present only when the service supplied one: it is omitted for on-device selections, when the service did not provide an explanation, and when a replayed decision made no routing call. The text is display-only and drawn from a fixed catalogue; several distinct routing categories share identical wording, so it cannot be used to recover the category or keyed on programmatically. + SelectionReason *string `json:"selectionReason,omitempty"` // Whether a sticky model choice overrode the router result StickyOverride *bool `json:"stickyOverride,omitempty"` } @@ -754,6 +762,21 @@ type SubagentSelectedData struct { func (*SubagentSelectedData) sessionEventData() {} func (*SubagentSelectedData) Type() SessionEventType { return SessionEventTypeSubagentSelected } +// Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. +// +// Deprecated: ToolExecutionPartialResultData is deprecated. +type ToolExecutionPartialResultData struct { + // Merged replacement snapshot from the running shell, not an append-only chunk + PartialOutput string `json:"partialOutput"` + // Tool call ID this partial result belongs to + ToolCallID string `json:"toolCallId"` +} + +func (*ToolExecutionPartialResultData) sessionEventData() {} +func (*ToolExecutionPartialResultData) Type() SessionEventType { + return SessionEventTypeToolExecutionPartialResult +} + // Durable record that a canvas instance is open, used to restore open canvases on cold session resume. Intentionally omits the transient url and availability. // Experimental: SessionCanvasRecordedData is part of an experimental API and may change or be removed. type SessionCanvasRecordedData struct { @@ -788,6 +811,23 @@ type SessionCanvasRemovedData struct { func (*SessionCanvasRemovedData) sessionEventData() {} func (*SessionCanvasRemovedData) Type() SessionEventType { return SessionEventTypeSessionCanvasRemoved } +// Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +type HumanResponseRecordedData struct { + // Controlled actor provenance established at response ingress. + Actor HumanResponseActor `json:"actor"` + // Request ID of the runtime-owned question or plan review. + RequestID string `json:"requestId"` + // Typed request and response payload. + Response HumanResponseRecordedResponse `json:"response"` + // Tool call ID that opened the request, when present. + ToolCallID *string `json:"toolCallId,omitempty"` +} + +func (*HumanResponseRecordedData) sessionEventData() {} +func (*HumanResponseRecordedData) Type() SessionEventType { + return SessionEventTypeHumanResponseRecorded +} + // Durable session usage checkpoint for reconstructing aggregate accounting on resume type SessionUsageCheckpointData struct { // Internal per-model prompt-cache state used to restore expiration tracking on resume @@ -1350,6 +1390,8 @@ type ModelCallFailureData struct { APIEndpoint *AssistantUsageAPIEndpoint `json:"apiEndpoint,omitempty"` // For HTTP 400 failures only: whether the response carried a structured CAPI error envelope (structured_error, a deterministic validation failure) or no error body (bodyless, the transient gateway/proxy signature). Absent for non-400 failures. BadRequestKind *ModelCallFailureBadRequestKind `json:"badRequestKind,omitempty"` + // Where the bring-your-own-key model for the failed call runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + ByokKind *string `json:"byokKind,omitempty"` // Duration of the failed API call in milliseconds DurationMs *int64 `json:"durationMs,omitempty"` // For HTTP 400 failures only: the `code` from the CAPI error envelope (e.g. 'model_max_prompt_tokens_exceeded') identifying which deterministic validation failure occurred. Raw server-controlled string, emitted only through restricted telemetry. Absent for bodyless or non-400 failures. @@ -1377,6 +1419,8 @@ type ModelCallFailureData struct { MaxPromptTokens *int64 `json:"maxPromptTokens,omitempty"` // Model identifier used for the failed API call Model *string `json:"model,omitempty"` + // Fixed-set provider family serving the bring-your-own-key model for the failed call (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + ModelProvider *string `json:"modelProvider,omitempty"` // Parent task tool call ID when this failed model call belongs to a sub-agent ParentToolCallID *string `json:"parentToolCallId,omitempty"` // GitHub request tracing ID (x-github-request-id header) for server-side log correlation @@ -1679,6 +1723,8 @@ type AssistantUsageData struct { // Number of tools available to the model for this call // Internal: AvailableToolCount is part of the SDK's internal API surface and is not intended for external use. AvailableToolCount *int64 `json:"availableToolCount,omitempty"` + // Where the bring-your-own-key model runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + ByokKind *string `json:"byokKind,omitempty"` // Whether the provider reported prompt-cache usage details for this call // Internal: CacheDetailsReported is part of the SDK's internal API surface and is not intended for external use. CacheDetailsReported *bool `json:"cacheDetailsReported,omitempty"` @@ -1726,6 +1772,8 @@ type AssistantUsageData struct { MaxPromptTokens *int64 `json:"maxPromptTokens,omitempty"` // Model identifier used for this API call Model string `json:"model"` + // Fixed-set provider family serving the bring-your-own-key model (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + ModelProvider *string `json:"modelProvider,omitempty"` // Number of tool calls returned by the model // Internal: NumToolCalls is part of the SDK's internal API surface and is not intended for external use. NumToolCalls *int64 `json:"numToolCalls,omitempty"` @@ -1789,6 +1837,21 @@ func (*AssistantServerToolProgressData) Type() SessionEventType { return SessionEventTypeAssistantServerToolProgress } +// Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +type ToolShellOutputData struct { + // Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + Sequence int64 `json:"sequence"` + // Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. + Stream *ToolShellOutputStream `json:"stream,omitempty"` + // New output to append, without synthetic shell-result markers or stream-switch separators + Text string `json:"text"` + // Tool call ID that owns this shell output + ToolCallID string `json:"toolCallId"` +} + +func (*ToolShellOutputData) sessionEventData() {} +func (*ToolShellOutputData) Type() SessionEventType { return SessionEventTypeToolShellOutput } + // Live-only Auto preference recommendation from Copilot API after a successful Auto model call. // Experimental: SessionAutoTierRecommendationData is part of an experimental API and may change or be removed. type SessionAutoTierRecommendationData struct { @@ -1937,6 +2000,8 @@ type SessionModelChangeData struct { PreviousVerbosity *Verbosity `json:"previousVerbosity,omitempty"` // Reasoning effort level after the model change, if applicable ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode after the model change, if applicable ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Origin of the effective model change, when known. @@ -2192,7 +2257,7 @@ func (*SessionMCPServerRemovedData) Type() SessionEventType { // Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. type SessionMCPServerStatusChangedData struct { - // Runtime configuration provenance for a failed connection, or unknown when unavailable. Additional string values may be introduced. + // Runtime configuration provenance for a connected or failed server, or unknown when unavailable. Additional string values may be introduced. ConfigSource *string `json:"configSource,omitempty"` // Error message if the server entered a failed state Error *string `json:"error,omitempty"` @@ -2612,6 +2677,8 @@ type SessionStartData struct { Producer string `json:"producer"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub @@ -2690,6 +2757,8 @@ type SessionResumeData struct { EventsFileSizeBytes *int64 `json:"eventsFileSizeBytes,omitempty"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` + // Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + ReasoningEffortModel *string `json:"reasoningEffortModel,omitempty"` // Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub @@ -2865,19 +2934,6 @@ func (*AssistantStreamingDeltaData) Type() SessionEventType { return SessionEventTypeAssistantStreamingDelta } -// Streaming tool execution output for incremental result display -type ToolExecutionPartialResultData struct { - // Incremental output chunk from the running tool - PartialOutput string `json:"partialOutput"` - // Tool call ID this partial result belongs to - ToolCallID string `json:"toolCallId"` -} - -func (*ToolExecutionPartialResultData) sessionEventData() {} -func (*ToolExecutionPartialResultData) Type() SessionEventType { - return SessionEventTypeToolExecutionPartialResult -} - // Streaming tool-call input delta for incremental tool-call updates type AssistantToolCallDeltaData struct { // Raw provider tool input fragment to append for this tool call. Function/tool-use providers stream serialized JSON argument text (so newlines inside JSON string values may appear as escaped `\n` until the accumulated JSON is parsed); custom tool calls stream raw custom input. @@ -3070,6 +3126,9 @@ func (*SessionModelDeselectedData) Type() SessionEventType { type ToolExecutionCompleteData struct { // Error details when the tool execution failed Error *ToolExecutionCompleteError `json:"error,omitempty"` + // Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + // Experimental: FileEdits is part of an experimental API and may change or be removed. + FileEdits []ToolExecutionCompleteFileEdit `json:"fileEdits,omitzero"` // 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"` @@ -3115,6 +3174,9 @@ func (*ToolExecutionCompleteData) Type() SessionEventType { type ToolExecutionProgressData struct { // Human-readable progress status message (e.g., from an MCP server) ProgressMessage string `json:"progressMessage"` + // Client-only structured progress metadata. Not model-facing tool output. + // Experimental: StructuredContent is part of an experimental API and may change or be removed. + StructuredContent any `json:"structuredContent,omitempty"` // Tool call ID this progress notification belongs to ToolCallID string `json:"toolCallId"` } @@ -3332,6 +3394,8 @@ func (*SessionWorkspaceFileChangedData) Type() SessionEventType { type AssistantMessageReasoningBlocks struct { // Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. Blocks []any `json:"blocks,omitzero"` + // Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + OrderedBlocks []any `json:"orderedBlocks,omitzero"` // Model provider that produced these reasoning blocks. Provider string `json:"provider"` } @@ -3872,6 +3936,78 @@ type HookEndError struct { Stack *string `json:"stack,omitempty"` } +// Exact runtime-owned question or reviewed plan paired with the typed response that settled it. +type HumanResponseRecordedResponse interface { + humanResponseRecordedResponse() + ResponseKind() HumanResponseRecordedResponseResponseKind +} + +type RawHumanResponseRecordedResponse struct { + Discriminator HumanResponseRecordedResponseResponseKind + Raw json.RawMessage +} + +func (RawHumanResponseRecordedResponse) humanResponseRecordedResponse() {} +func (r RawHumanResponseRecordedResponse) ResponseKind() HumanResponseRecordedResponseResponseKind { + return r.Discriminator +} + +type HumanResponseRecordedResponseAskUser struct { + // Exact answer content accepted from the user. + Content map[string]any `json:"content"` + // Exact question displayed to the user. + Message string `json:"message"` + // Exact response schema displayed to the user. + RequestedSchema ElicitationRequestedSchema `json:"requestedSchema"` +} + +func (HumanResponseRecordedResponseAskUser) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseAskUser) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindAskUser +} + +type HumanResponseRecordedResponseExitPlanMode struct { + // Actions offered by the plan review UI. + Actions []ExitPlanModeAction `json:"actions"` + // Whether the user approved the reviewed plan. + Approved bool `json:"approved"` + // Whether the selected response requested edit auto-approval. + AutoApproveEdits *bool `json:"autoApproveEdits,omitempty"` + // Exact feedback submitted with the plan decision, when present. + Feedback *string `json:"feedback,omitempty"` + // Exact full plan content available from the review UI. + PlanContent string `json:"planContent"` + // Action the plan review UI recommended. + RecommendedAction ExitPlanModeAction `json:"recommendedAction"` + // Action selected by the user, when applicable. + SelectedAction *ExitPlanModeAction `json:"selectedAction,omitempty"` + // Exact plan summary displayed to the user. + Summary string `json:"summary"` +} + +func (HumanResponseRecordedResponseExitPlanMode) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseExitPlanMode) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindExitPlanMode +} + +type HumanResponseRecordedResponseUserInput struct { + // Whether the displayed request allowed a free-form answer. + AllowFreeform *bool `json:"allowFreeform,omitempty"` + // Exact selected or free-form answer submitted by the user. + Answer string `json:"answer"` + // Exact choices displayed to the user, when the request offered choices. + Choices []string `json:"choices,omitzero"` + // Exact question displayed to the user. + Question string `json:"question"` + // Whether the answer was typed as free-form text rather than selected from the displayed choices. + WasFreeform bool `json:"wasFreeform"` +} + +func (HumanResponseRecordedResponseUserInput) humanResponseRecordedResponse() {} +func (HumanResponseRecordedResponseUserInput) ResponseKind() HumanResponseRecordedResponseResponseKind { + return HumanResponseRecordedResponseResponseKindUserInput +} + // Set when the underlying tools/call threw an error before returning a CallToolResult type MCPAppToolCallCompleteError struct { // Human-readable error message @@ -5399,6 +5535,15 @@ type ToolExecutionCompleteError struct { Remediation *RemediationAction `json:"remediation,omitempty"` } +// A file mutation that was actually committed by a built-in file editing tool. +// Experimental: ToolExecutionCompleteFileEdit is part of an experimental API and may change or be removed. +type ToolExecutionCompleteFileEdit struct { + // Kind of mutation committed at this path. + Kind ToolExecutionCompleteFileEditKind `json:"kind"` + // Absolute path in the session filesystem namespace. + Path string `json:"path"` +} + // Tool execution result on success type ToolExecutionCompleteResult struct { // Model-facing binary results (base64 inline or size-omitted markers) sent to the LLM for this tool call @@ -6037,6 +6182,27 @@ const ( HandoffSourceTypeRemote HandoffSourceType = "remote" ) +// Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. +type HumanResponseActor string + +const ( + // A host or SDK automation submitted the response without direct human interaction. + HumanResponseActorHostAutomation HumanResponseActor = "host_automation" + // A built-in trusted client submitted the response after direct human interaction. + HumanResponseActorHumanResponse HumanResponseActor = "human_response" + // The response came through a legacy or otherwise unattributed ingress. + HumanResponseActorUnknown HumanResponseActor = "unknown" +) + +// ResponseKind discriminator for HumanResponseRecordedResponse. +type HumanResponseRecordedResponseResponseKind string + +const ( + HumanResponseRecordedResponseResponseKindAskUser HumanResponseRecordedResponseResponseKind = "ask_user" + HumanResponseRecordedResponseResponseKindExitPlanMode HumanResponseRecordedResponseResponseKind = "exit_plan_mode" + HumanResponseRecordedResponseResponseKindUserInput HumanResponseRecordedResponseResponseKind = "user_input" +) + // The category of runtime action that enterprise managed settings governed (blocked or capped) type ManagedSettingsEnforcedAction string @@ -6274,6 +6440,14 @@ const ( PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable PermissionApprovalEvaluationReasonCode = "argument-binding-unreviewable" // The judge was skipped because authorization extraction could not safely establish a complete recent history. PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete PermissionApprovalEvaluationReasonCode = "authorization-history-incomplete" + // A code source was excluded from review by content exclusion policy. + PermissionApprovalEvaluationReasonCodeContentExcluded PermissionApprovalEvaluationReasonCode = "content-excluded" + // The shell command used a code source computed at run time. + PermissionApprovalEvaluationReasonCodeDynamicSource PermissionApprovalEvaluationReasonCode = "dynamic-source" + // A code-bearing executable exceeded the binding size limit. + PermissionApprovalEvaluationReasonCodeExecutableTooLarge PermissionApprovalEvaluationReasonCode = "executable-too-large" + // A code-bearing executable could not be inspected. + PermissionApprovalEvaluationReasonCodeExecutableUnavailable PermissionApprovalEvaluationReasonCode = "executable-unavailable" // Assisted approval was inactive for this request. PermissionApprovalEvaluationReasonCodeInactive PermissionApprovalEvaluationReasonCode = "inactive" // The request inherited an outcome from another decision. @@ -6310,6 +6484,8 @@ const ( PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable PermissionApprovalEvaluationReasonCode = "shell-environment-unreviewable" // The script snapshot exceeded the size limit. PermissionApprovalEvaluationReasonCodeTooLarge PermissionApprovalEvaluationReasonCode = "too-large" + // The shell command referenced more code sources than can be reviewed. + PermissionApprovalEvaluationReasonCodeTooManySources PermissionApprovalEvaluationReasonCode = "too-many-sources" // Script review was unavailable. PermissionApprovalEvaluationReasonCodeUnavailable PermissionApprovalEvaluationReasonCode = "unavailable" // Attribution is missing or outside the supported vocabulary. @@ -6320,6 +6496,10 @@ const ( PermissionApprovalEvaluationReasonCodeUnrepresentablePath PermissionApprovalEvaluationReasonCode = "unrepresentable-path" // The script invocation could not be reviewed. PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation PermissionApprovalEvaluationReasonCode = "unreviewable-script-invocation" + // The shell command could not be analyzed for execution evidence. + PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape PermissionApprovalEvaluationReasonCode = "unsupported-command-shape" + // The shell command used a code source that cannot be bound for review. + PermissionApprovalEvaluationReasonCodeUnsupportedSource PermissionApprovalEvaluationReasonCode = "unsupported-source" ) // Direction stored in a historical extractor claim. Current runtimes do not apply it. @@ -6641,6 +6821,19 @@ const ( ToolExecutionCompleteContentTypeText ToolExecutionCompleteContentType = "text" ) +// Kind of file mutation committed by a built-in editing tool. +// Experimental: ToolExecutionCompleteFileEditKind is part of an experimental API and may change or be removed. +type ToolExecutionCompleteFileEditKind string + +const ( + // A file was created. + ToolExecutionCompleteFileEditKindCreate ToolExecutionCompleteFileEditKind = "create" + // A file was deleted. + ToolExecutionCompleteFileEditKindDelete ToolExecutionCompleteFileEditKind = "delete" + // A file was written by an edit operation. + ToolExecutionCompleteFileEditKindEdit ToolExecutionCompleteFileEditKind = "edit" +) + // Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration. type ToolExecutionCompleteToolDescriptionMetaUIVisibility string @@ -6661,6 +6854,18 @@ const ( ToolExecutionStartToolDescriptionMetaUIVisibilityModel ToolExecutionStartToolDescriptionMetaUIVisibility = "model" ) +// Shell output source. Terminal output has no separate stdout/stderr attribution. +type ToolShellOutputStream string + +const ( + // Output from the shell command's standard error stream. + ToolShellOutputStreamStderr ToolShellOutputStream = "stderr" + // Output from the shell command's standard output stream. This is the default when stream is omitted. + ToolShellOutputStreamStdout ToolShellOutputStream = "stdout" + // Inherently merged output that cannot be attributed separately to stdout or stderr. + ToolShellOutputStreamTerminal ToolShellOutputStream = "terminal" +) + // Lifecycle phase for a Rust-owned ephemeral query stream. // Experimental: UIEphemeralQueryPhase is part of an experimental API and may change or be removed. type UIEphemeralQueryPhase string diff --git a/go/session.go b/go/session.go index f4e7696518..bfc618f422 100644 --- a/go/session.go +++ b/go/session.go @@ -59,6 +59,7 @@ type Session struct { // SessionID is the unique identifier for this session. SessionID string workspacePath string + transcriptRecovery *TranscriptRecoveryReport client *jsonrpc2.Client clientSessionAPIs *rpc.ClientSessionAPIHandlers handlers []sessionHandler @@ -66,6 +67,8 @@ type Session struct { handlerMutex sync.RWMutex toolHandlers map[string]ToolHandler toolHandlersM sync.RWMutex + setToolsOnce sync.Once + setToolsGate chan struct{} pendingExternalTools map[string]*pendingExternalTool pendingExternalToolsM sync.Mutex externalToolsClosed bool @@ -90,6 +93,8 @@ type Session struct { elicitationMu sync.RWMutex canvasHandler CanvasHandler canvasMu sync.RWMutex + skillProvider SkillProvider + skillProviderMu sync.RWMutex bearerTokenProviders map[string]BearerTokenProvider bearerTokenMu sync.RWMutex releaseGitHubTokenProvider func() @@ -123,6 +128,17 @@ func (s *Session) WorkspacePath() string { return s.workspacePath } +// TranscriptRecovery returns the report from session.resume, or nil if no +// transcript repair was reported. The returned value is independent of session state. +func (s *Session) TranscriptRecovery() *TranscriptRecoveryReport { + if s.transcriptRecovery == nil { + return nil + } + report := *s.transcriptRecovery + report.InvalidLineNumbers = append([]int(nil), report.InvalidLineNumbers...) + return &report +} + // OpenCanvases returns the open-canvas snapshot last reported by the runtime. // The snapshot is populated from session.resume and live session.canvas.opened // and session.canvas.closed events. The returned slice is a copy and is safe to @@ -207,6 +223,24 @@ func (s *Session) getCanvasHandler() CanvasHandler { return s.canvasHandler } +func (s *Session) registerSkillProvider(provider SkillProvider) { + s.skillProviderMu.Lock() + defer s.skillProviderMu.Unlock() + s.skillProvider = provider +} + +func (s *Session) getSkillProvider() SkillProvider { + s.skillProviderMu.RLock() + defer s.skillProviderMu.RUnlock() + return s.skillProvider +} + +func (s *Session) clearSkillProvider() { + s.skillProviderMu.Lock() + s.skillProvider = nil + s.skillProviderMu.Unlock() +} + // registerBearerTokenProviders installs per-provider [BearerTokenProvider] callbacks // for BYOK providers configured with managed-identity / on-demand bearer-token // auth, keyed by provider name. @@ -624,16 +658,24 @@ func (s *Session) On(handler SessionEventHandler) func() { // // This method is internal and typically called when creating a session with tools. func (s *Session) registerTools(tools []Tool) { - s.toolHandlersM.Lock() - defer s.toolHandlersM.Unlock() + s.replaceToolHandlers(buildToolHandlerMap(tools)) +} - s.toolHandlers = make(map[string]ToolHandler) +func buildToolHandlerMap(tools []Tool) map[string]ToolHandler { + handlers := make(map[string]ToolHandler) for _, tool := range tools { if tool.Name == "" || tool.Handler == nil { continue } - s.toolHandlers[tool.Name] = tool.Handler + handlers[tool.Name] = tool.Handler } + return handlers +} + +func (s *Session) replaceToolHandlers(handlers map[string]ToolHandler) { + s.toolHandlersM.Lock() + defer s.toolHandlersM.Unlock() + s.toolHandlers = handlers } // getToolHandler retrieves a registered tool handler by name. @@ -874,6 +916,26 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) ( } return hooks.OnAgentStop(input, invocation) + case "subagentStart": + if hooks.OnSubagentStart == nil { + return nil, nil + } + var input SubagentStartHookInput + if err := json.Unmarshal(rawInput, &input); err != nil { + return nil, fmt.Errorf("invalid hook input: %w", err) + } + return hooks.OnSubagentStart(input, invocation) + + case "subagentStop": + if hooks.OnSubagentStop == nil { + return nil, nil + } + var input SubagentStopHookInput + if err := json.Unmarshal(rawInput, &input); err != nil { + return nil, fmt.Errorf("invalid hook input: %w", err) + } + return hooks.OnSubagentStop(input, invocation) + default: return nil, nil } @@ -1478,6 +1540,7 @@ func (s *Session) processEvents() { // CreateSession/ResumeSession use this when a locally registered session fails // before it can be returned to the caller. func (s *Session) stopEventProcessing() { + s.clearSkillProvider() s.closeOnce.Do(func() { close(s.eventDone) }) } @@ -1853,6 +1916,7 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) { // log.Printf("Failed to disconnect session: %v", err) // } func (s *Session) Disconnect() error { + s.clearSkillProvider() s.cancelPendingExternalTools() result, err := s.client.Request(context.Background(), "session.detach", sessionDetachRequest{SessionID: s.SessionID}) if err == nil { @@ -2029,6 +2093,108 @@ func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOpti return nil } +func (s *Session) setToolsSemaphore() chan struct{} { + s.setToolsOnce.Do(func() { + s.setToolsGate = make(chan struct{}, 1) + s.setToolsGate <- struct{}{} + }) + return s.setToolsGate +} + +func toolDefinitionsForSetTools(tools []Tool) []rpc.ProtocolExternalToolDefinition { + definitions := make([]rpc.ProtocolExternalToolDefinition, 0, len(tools)) + for _, tool := range tools { + definition := rpc.ProtocolExternalToolDefinition{ + Name: tool.Name, + Description: tool.Description, + Parameters: tool.Parameters, + Metadata: tool.Metadata, + } + if tool.OverridesBuiltInTool { + definition.OverridesBuiltInTool = &tool.OverridesBuiltInTool + } + if tool.SkipPermission { + definition.SkipPermission = &tool.SkipPermission + } + if tool.IsTerminal { + definition.IsTerminal = &tool.IsTerminal + } + if tool.Defer != "" { + deferPolicy := rpc.ProtocolExternalToolDefer(tool.Defer) + definition.Defer = &deferPolicy + } + definitions = append(definitions, definition) + } + return definitions +} + +// SetTools atomically replaces the externally implemented tools supplied by +// this client connection for the live session. Built-in tools, MCP/plugin tools, +// extension-discovered tools, subagent tools, and tools supplied by other +// connections are unchanged. +// +// The tools use the same [Tool] definitions and handlers as +// [Client.CreateSession] and [Client.ResumeSession]. The slice is a complete +// replacement for this connection: pass an empty slice to remove every tool this +// connection previously supplied. Tools with nil handlers are declaration-only. +// +// Handlers switch after the runtime accepts the replacement. Calls already +// running finish on the handlers that started them. If the runtime rejects the +// replacement, the previous handlers remain installed. Concurrent SetTools calls +// on the same session are sent and applied in call order; an earlier failure does +// not block later calls. If ctx is done before the request is sent, for example +// while an earlier SetTools call is still in flight, nothing is sent. Once the +// request is sent, ctx only bounds the wait: an accepted replacement still +// installs its handlers. +// +// The agent sees the new tools from its next model request, which can fall +// within a turn in progress. A model request already in flight was made with the +// previous tools, so the agent can still call a tool you removed. This session +// doesn't answer that call, and it can stay pending until the turn is aborted. If +// a running turn might still call a tool you remove, replace tools while the +// session is idle. +// +// Experimental: SetTools wraps the experimental session.tools.set RPC and may +// change or be removed. +func (s *Session) SetTools(ctx context.Context, tools []Tool) error { + definitions := toolDefinitionsForSetTools(tools) + handlers := buildToolHandlerMap(tools) + + gate := s.setToolsSemaphore() + select { + case <-gate: + case <-ctx.Done(): + return ctx.Err() + } + + if err := ctx.Err(); err != nil { + gate <- struct{}{} + return err + } + + done := make(chan error, 1) + go func() { + defer func() { gate <- struct{}{} }() + + _, err := s.RPC.Tools.Set(context.WithoutCancel(ctx), &rpc.ToolsSetRequest{ + Tools: definitions, + }) + if err != nil { + done <- fmt.Errorf("failed to set tools: %w", err) + return + } + s.replaceToolHandlers(handlers) + done <- nil + }() + + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() + } +} + // SetAutoTier changes the Auto routing preference without changing the selected model. // // The runtime does not apply the preference immediately. It records the request and diff --git a/go/session_fs_provider.go b/go/session_fs_provider.go index 0f653f1a0f..1f00ecc83f 100644 --- a/go/session_fs_provider.go +++ b/go/session_fs_provider.go @@ -5,6 +5,7 @@ package copilot import ( + "encoding/base64" "errors" "os" "time" @@ -12,6 +13,9 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) +const maxSessionFSBinaryBytes = (64*1024*1024 - 1024) / 4 * 3 +const maxSessionFSBinaryContentLength = (maxSessionFSBinaryBytes + 2) / 3 * 4 + // SessionFSProvider is the interface that SDK users implement to provide // a session filesystem. Methods use idiomatic Go error handling: return an // error for failures (the adapter maps os.ErrNotExist → ENOENT automatically). @@ -23,6 +27,7 @@ type SessionFSProvider interface { ReadFile(path string) (string, error) // WriteFile writes content to a file, creating it and parent directories if needed. // mode is an optional POSIX-style permission mode. Pass nil to use the OS default. + // Return a *SessionFSWriteFailure if the failed write changed the target. WriteFile(path string, content string, mode *int) error // AppendFile appends content to a file, creating it and parent directories if needed. // mode is an optional POSIX-style permission mode. Pass nil to use the OS default. @@ -48,6 +53,32 @@ type SessionFSProvider interface { Rename(src string, dest string) error } +// SessionFSWriteFailure reports that a failed WriteFile changed its target. +// Wrap the original error so its cause remains available to errors.Is and errors.As. +type SessionFSWriteFailure struct { + Err error +} + +func (e *SessionFSWriteFailure) Error() string { + if e == nil || e.Err == nil { + return "session filesystem write failed after changing the target" + } + return e.Err.Error() +} +func (e *SessionFSWriteFailure) Unwrap() error { + if e == nil { + return nil + } + return e.Err +} + +// SessionFSBinaryProvider is an optional interface for exact file bytes. +// Declare capabilities.binary only when the provider implements both methods. +type SessionFSBinaryProvider interface { + ReadFileBytes(path string) ([]byte, error) + WriteFileBytes(path string, content []byte, mode *int) error +} + // SessionFSSqliteProvider is an optional interface that a [SessionFSProvider] // may also implement to support per-session SQLite databases. The adapter // checks for this interface at runtime using a type assertion. If the @@ -129,6 +160,23 @@ func (a *sessionFSAdapter) ReadFile(request *rpc.SessionFSReadFileRequest) (*rpc return &rpc.SessionFSReadFileResult{Content: content}, nil } +func (a *sessionFSAdapter) ReadFileBytes(request *rpc.SessionFSReadFileBytesRequest) (*rpc.SessionFSReadFileBytesResult, error) { + provider, ok := a.provider.(SessionFSBinaryProvider) + if !ok { + return &rpc.SessionFSReadFileBytesResult{Error: toSessionFSError(errors.New("binary reads are not supported"))}, nil + } + bytes, err := provider.ReadFileBytes(request.Path) + if err != nil { + return &rpc.SessionFSReadFileBytesResult{Error: toSessionFSError(err)}, nil + } + if len(bytes) > maxSessionFSBinaryBytes { + return &rpc.SessionFSReadFileBytesResult{ + Error: toSessionFSError(errors.New("sessionFs.readFileBytes content exceeds the binary read limit")), + }, nil + } + return &rpc.SessionFSReadFileBytesResult{Content: base64.StdEncoding.EncodeToString(bytes)}, nil +} + func (a *sessionFSAdapter) WriteFile(request *rpc.SessionFSWriteFileRequest) (*rpc.SessionFSError, error) { var mode *int if request.Mode != nil { @@ -136,6 +184,41 @@ func (a *sessionFSAdapter) WriteFile(request *rpc.SessionFSWriteFileRequest) (*r mode = &m } if err := a.provider.WriteFile(request.Path, request.Content, mode); err != nil { + wire := toSessionFSError(err) + var changed *SessionFSWriteFailure + if errors.As(err, &changed) && changed != nil { + value := true + wire.WriteChanged = &value + } + return wire, nil + } + return nil, nil +} + +func (a *sessionFSAdapter) WriteFileBytes(request *rpc.SessionFSWriteFileBytesRequest) (*rpc.SessionFSError, error) { + provider, ok := a.provider.(SessionFSBinaryProvider) + if !ok { + return toSessionFSError(errors.New("binary writes are not supported")), nil + } + if len(request.Content) > maxSessionFSBinaryContentLength { + return toSessionFSError(errors.New("sessionFs.writeFileBytes content exceeds the binary write limit")), nil + } + content, err := base64.StdEncoding.Strict().DecodeString(request.Content) + if err != nil { + return toSessionFSError(err), nil + } + if base64.StdEncoding.EncodeToString(content) != request.Content { + return toSessionFSError(errors.New("invalid sessionFs.writeFileBytes base64 content")), nil + } + if len(content) > maxSessionFSBinaryBytes { + return toSessionFSError(errors.New("sessionFs.writeFileBytes content exceeds the binary write limit")), nil + } + var mode *int + if request.Mode != nil { + m := int(*request.Mode) + mode = &m + } + if err := provider.WriteFileBytes(request.Path, content, mode); err != nil { return toSessionFSError(err), nil } return nil, nil diff --git a/go/session_fs_provider_test.go b/go/session_fs_provider_test.go new file mode 100644 index 0000000000..e0b3d3aba0 --- /dev/null +++ b/go/session_fs_provider_test.go @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/github/copilot-sdk/go/rpc" +) + +type failingWriteProvider struct { + SessionFSProvider + err error +} + +func (p failingWriteProvider) WriteFile(string, string, *int) error { return p.err } +func (p failingWriteProvider) AppendFile(string, string, *int) error { return p.err } + +func TestSessionFSWriteFailure(t *testing.T) { + for _, tc := range []struct { + name string + err error + wantChanged bool + wantCode rpc.SessionFSErrorCode + wantMessage string + }{ + {"rejected", errors.New("rejected"), false, rpc.SessionFSErrorCodeUNKNOWN, "rejected"}, + {"partial", &SessionFSWriteFailure{Err: errors.New("disk full")}, true, rpc.SessionFSErrorCodeUNKNOWN, "disk full"}, + {"wrapped", fmt.Errorf("write failed: %w", &SessionFSWriteFailure{Err: os.ErrNotExist}), true, rpc.SessionFSErrorCodeENOENT, "write failed: file does not exist"}, + {"nil cause", &SessionFSWriteFailure{}, true, rpc.SessionFSErrorCodeUNKNOWN, "session filesystem write failed after changing the target"}, + {"nil marker", (*SessionFSWriteFailure)(nil), false, rpc.SessionFSErrorCodeUNKNOWN, "session filesystem write failed after changing the target"}, + } { + t.Run(tc.name, func(t *testing.T) { + adapter := newSessionFSAdapter(failingWriteProvider{err: tc.err}) + wire, err := adapter.WriteFile(&rpc.SessionFSWriteFileRequest{Path: "/file", Content: "data"}) + if err != nil || wire == nil { + t.Fatalf("WriteFile = %v, %v; want wire error", wire, err) + } + if wire.Code != tc.wantCode { + t.Errorf("code = %v; want %v", wire.Code, tc.wantCode) + } + if wire.Message == nil || *wire.Message != tc.wantMessage { + t.Errorf("message = %v; want %q", wire.Message, tc.wantMessage) + } + gotChanged := wire.WriteChanged != nil && *wire.WriteChanged + if gotChanged != tc.wantChanged || !tc.wantChanged && wire.WriteChanged != nil { + t.Errorf("writeChanged = %v; want %v", wire.WriteChanged, tc.wantChanged) + } + }) + } +} + +func TestSessionFSWriteFailureDoesNotMarkAppend(t *testing.T) { + adapter := newSessionFSAdapter(failingWriteProvider{ + err: &SessionFSWriteFailure{Err: errors.New("append failed")}, + }) + wire, err := adapter.AppendFile(&rpc.SessionFSAppendFileRequest{Path: "/file", Content: "data"}) + if err != nil || wire == nil || wire.WriteChanged != nil { + t.Fatalf("AppendFile = %v, %v; want unmarked wire error", wire, err) + } +} diff --git a/go/session_test.go b/go/session_test.go index 60e51308e6..b66643544b 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" "io" "strconv" @@ -184,6 +185,321 @@ func TestSession_SetAutoTierSendsExplicitNull(t *testing.T) { } } +func TestSession_SetToolsSendsPayloadAndInstallsHandlers(t *testing.T) { + session, server := newSetToolsTestSession(t) + handler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "handled"}, nil + } + tools := []Tool{ + { + Name: "lookup_fruit", + Parameters: map[string]any{"type": "object"}, + OverridesBuiltInTool: true, + SkipPermission: true, + IsTerminal: true, + Defer: ToolDeferNever, + Metadata: map[string]any{"x-test": "metadata"}, + Handler: handler, + }, + { + Name: "declaration_only", + Description: "No handler", + }, + } + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), tools) + }() + + request := server.expectRequest(t, "session.tools.set") + if request.Params["sessionId"] != "session-1" { + t.Fatalf("sessionId = %v, want session-1", request.Params["sessionId"]) + } + wireTools, ok := request.Params["tools"].([]any) + if !ok || len(wireTools) != 2 { + t.Fatalf("tools = %#v, want two tools", request.Params["tools"]) + } + first, ok := wireTools[0].(map[string]any) + if !ok { + t.Fatalf("first tool = %#v, want object", wireTools[0]) + } + if first["name"] != "lookup_fruit" { + t.Fatalf("name = %v, want lookup_fruit", first["name"]) + } + if description, ok := first["description"].(string); !ok || description != "" { + t.Fatalf("description = %#v, want empty string", first["description"]) + } + for _, key := range []string{"overridesBuiltInTool", "skipPermission", "isTerminal"} { + if first[key] != true { + t.Fatalf("%s = %v, want true", key, first[key]) + } + } + if first["defer"] != "never" { + t.Fatalf("defer = %v, want never", first["defer"]) + } + if _, ok := first["parameters"].(map[string]any); !ok { + t.Fatalf("parameters = %#v, want object", first["parameters"]) + } + if _, ok := first["metadata"].(map[string]any); !ok { + t.Fatalf("metadata = %#v, want object", first["metadata"]) + } + + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + if got, ok := session.getToolHandler("lookup_fruit"); !ok || got == nil { + t.Fatal("lookup_fruit handler was not installed") + } + if _, ok := session.getToolHandler("declaration_only"); ok { + t.Fatal("declaration-only tool installed a handler") + } +} + +func TestSession_SetToolsSwitchesHandlersAfterAccepted(t *testing.T) { + session, server := newSetToolsTestSession(t) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + + handler, ok := session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing while replacement RPC is pending") + } + result, err := handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "old" { + t.Fatalf("pending replacement used %q, %v; want old handler", result.TextResultForLLM, err) + } + + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + handler, ok = session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing after accepted replacement") + } + result, err = handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "new" { + t.Fatalf("accepted replacement used %q, %v; want new handler", result.TextResultForLLM, err) + } +} + +func TestSession_SetToolsRunningCallsFinishOnOriginalHandler(t *testing.T) { + session, server := newSetToolsTestSession(t) + started := make(chan struct{}) + release := make(chan struct{}) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + close(started) + <-release + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + session.startExternalTool(&ExternalToolRequestedData{ + RequestID: "request-1", + SessionID: "session-1", + ToolCallID: "tool-call-1", + ToolName: "lookup", + }) + select { + case <-started: + case <-time.After(2 * time.Second): + t.Fatal("tool handler did not start") + } + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + close(release) + + completion := server.expectRequest(t, "session.tools.handlePendingToolCall") + result, ok := completion.Params["result"].(map[string]any) + if !ok { + t.Fatalf("completion result = %#v, want object", completion.Params["result"]) + } + if result["textResultForLlm"] != "old" { + t.Fatalf("completion result = %#v, want old handler result", result) + } + server.respond(t, completion.ID, map[string]any{}) +} + +func TestSession_SetToolsRejectionLeavesHandlersUnchanged(t *testing.T) { + session, server := newSetToolsTestSession(t) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + server.respondError(t, request.ID, -32602, "invalid tool name") + if err := awaitSetTools(t, errCh); err == nil { + t.Fatal("SetTools succeeded; want rejection error") + } + + handler, ok := session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing after rejected replacement") + } + result, err := handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "old" { + t.Fatalf("rejected replacement used %q, %v; want old handler", result.TextResultForLLM, err) + } +} + +func TestSession_SetToolsEmptySetRemovesHandlers(t *testing.T) { + session, server := newSetToolsTestSession(t) + session.registerTools([]Tool{{Name: "lookup", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + }}}) + + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(context.Background(), []Tool{}) + }() + request := server.expectRequest(t, "session.tools.set") + wireTools, ok := request.Params["tools"].([]any) + if !ok || len(wireTools) != 0 { + t.Fatalf("tools = %#v, want empty list", request.Params["tools"]) + } + server.respond(t, request.ID, map[string]any{}) + if err := awaitSetTools(t, errCh); err != nil { + t.Fatalf("SetTools failed: %v", err) + } + if _, ok := session.getToolHandler("lookup"); ok { + t.Fatal("handler remained after empty replacement") + } +} + +func TestSession_SetToolsConcurrentCallsAreSerialized(t *testing.T) { + session, server := newSetToolsTestSession(t) + + firstErr := make(chan error, 1) + go func() { + firstErr <- session.SetTools(context.Background(), []Tool{{Name: "first", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + first := server.expectRequest(t, "session.tools.set") + + secondErr := make(chan error, 1) + go func() { + secondErr <- session.SetTools(context.Background(), []Tool{{Name: "second", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + + server.expectNoRequest(t) + server.respondError(t, first.ID, -32602, "first rejected") + if err := awaitSetTools(t, firstErr); err == nil { + t.Fatal("first SetTools succeeded; want rejection error") + } + + second := server.expectRequest(t, "session.tools.set") + server.respond(t, second.ID, map[string]any{}) + if err := awaitSetTools(t, secondErr); err != nil { + t.Fatalf("second SetTools failed: %v", err) + } + if _, ok := session.getToolHandler("first"); ok { + t.Fatal("first handler installed after rejected replacement") + } + if _, ok := session.getToolHandler("second"); !ok { + t.Fatal("second handler was not installed after earlier rejection") + } +} + +func TestSession_SetToolsCancelledAfterSendStillInstallsAcceptedHandlers(t *testing.T) { + session, server := newSetToolsTestSession(t) + oldHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "old"}, nil + } + newHandler := func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{TextResultForLLM: "new"}, nil + } + session.registerTools([]Tool{{Name: "lookup", Handler: oldHandler}}) + + ctx, cancel := context.WithCancel(context.Background()) + errCh := make(chan error, 1) + go func() { + errCh <- session.SetTools(ctx, []Tool{{Name: "lookup", Handler: newHandler}}) + }() + request := server.expectRequest(t, "session.tools.set") + cancel() + if err := awaitSetTools(t, errCh); !errors.Is(err, context.Canceled) { + t.Fatalf("SetTools error = %v, want context.Canceled", err) + } + handler, ok := session.getToolHandler("lookup") + if !ok { + t.Fatal("lookup handler missing before accepted replacement resolves") + } + result, err := handler(ToolInvocation{}) + if err != nil || result.TextResultForLLM != "old" { + t.Fatalf("cancelled pending replacement used %q, %v; want old handler", result.TextResultForLLM, err) + } + + server.respond(t, request.ID, map[string]any{}) + waitForToolHandlerResult(t, session, "lookup", "new") +} + +func TestSession_SetToolsAlreadyCancelledWhileBlockedDoesNotSend(t *testing.T) { + session, server := newSetToolsTestSession(t) + firstErr := make(chan error, 1) + go func() { + firstErr <- session.SetTools(context.Background(), []Tool{{Name: "first", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + first := server.expectRequest(t, "session.tools.set") + + ctx, cancel := context.WithCancel(context.Background()) + secondErr := make(chan error, 1) + go func() { + secondErr <- session.SetTools(ctx, []Tool{{Name: "second", Handler: func(_ ToolInvocation) (ToolResult, error) { + return ToolResult{}, nil + }}}) + }() + cancel() + if err := awaitSetTools(t, secondErr); !errors.Is(err, context.Canceled) { + t.Fatalf("blocked SetTools error = %v, want context.Canceled", err) + } + server.expectNoRequest(t) + + server.respond(t, first.ID, map[string]any{}) + if err := awaitSetTools(t, firstErr); err != nil { + t.Fatalf("first SetTools failed: %v", err) + } + server.expectNoRequest(t) + if _, ok := session.getToolHandler("second"); ok { + t.Fatal("cancelled blocked replacement installed a handler") + } +} + func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() @@ -521,6 +837,169 @@ func readTestJSONRPCFrame(r io.Reader) ([]byte, error) { return data, err } +type setToolsJSONRPCRequest struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` +} + +type setToolsTestServer struct { + stdinR *io.PipeReader + stdinW *io.PipeWriter + stdoutR *io.PipeReader + stdoutW *io.PipeWriter + client *jsonrpc2.Client + mu sync.Mutex + request chan setToolsJSONRPCRequest + errs chan error +} + +func newSetToolsTestSession(t *testing.T) (*Session, *setToolsTestServer) { + t.Helper() + + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + client := jsonrpc2.NewClient(stdinW, stdoutR) + client.Start() + + server := &setToolsTestServer{ + stdinR: stdinR, + stdinW: stdinW, + stdoutR: stdoutR, + stdoutW: stdoutW, + client: client, + request: make(chan setToolsJSONRPCRequest, 16), + errs: make(chan error, 1), + } + go server.readRequests() + t.Cleanup(func() { + client.Stop() + stdinR.Close() + stdinW.Close() + stdoutR.Close() + stdoutW.Close() + }) + + session := &Session{ + SessionID: "session-1", + client: client, + toolHandlers: make(map[string]ToolHandler), + RPC: rpc.NewSessionRPC(client, "session-1"), + } + return session, server +} + +func (s *setToolsTestServer) readRequests() { + for { + frame, err := readTestJSONRPCFrame(s.stdinR) + if err != nil { + return + } + + var request setToolsJSONRPCRequest + if err := json.Unmarshal(frame, &request); err != nil { + select { + case s.errs <- err: + default: + } + return + } + s.request <- request + } +} + +func (s *setToolsTestServer) expectRequest(t *testing.T, method string) setToolsJSONRPCRequest { + t.Helper() + select { + case request := <-s.request: + if request.Method != method { + t.Fatalf("method = %s, want %s", request.Method, method) + } + return request + case err := <-s.errs: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatalf("timed out waiting for %s request", method) + } + return setToolsJSONRPCRequest{} +} + +func (s *setToolsTestServer) expectNoRequest(t *testing.T) { + t.Helper() + select { + case request := <-s.request: + t.Fatalf("unexpected request while replacement should be blocked: %s", request.Method) + case err := <-s.errs: + t.Fatal(err) + case <-time.After(100 * time.Millisecond): + } +} + +func (s *setToolsTestServer) respond(t *testing.T, id json.RawMessage, result any) { + t.Helper() + s.writeResponse(t, map[string]any{ + "jsonrpc": "2.0", + "id": id, + "result": result, + }) +} + +func (s *setToolsTestServer) respondError(t *testing.T, id json.RawMessage, code int, message string) { + t.Helper() + s.writeResponse(t, map[string]any{ + "jsonrpc": "2.0", + "id": id, + "error": map[string]any{ + "code": code, + "message": message, + }, + }) +} + +func (s *setToolsTestServer) writeResponse(t *testing.T, response map[string]any) { + t.Helper() + data, err := json.Marshal(response) + if err != nil { + t.Fatal(err) + } + s.mu.Lock() + defer s.mu.Unlock() + if _, err := fmt.Fprintf(s.stdoutW, "Content-Length: %d\r\n\r\n%s", len(data), data); err != nil { + t.Fatal(err) + } +} + +func awaitSetTools(t *testing.T, errCh <-chan error) error { + t.Helper() + select { + case err := <-errCh: + return err + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for SetTools") + } + return nil +} + +func waitForToolHandlerResult(t *testing.T, session *Session, name, want string) { + t.Helper() + deadline := time.After(2 * time.Second) + for { + handler, ok := session.getToolHandler(name) + if ok { + result, err := handler(ToolInvocation{}) + if err == nil && result.TextResultForLLM == want { + return + } + } + select { + case <-deadline: + t.Fatalf("timed out waiting for %s handler to return %q", name, want) + default: + time.Sleep(time.Millisecond) + } + } +} + func TestSession_SendAndWaitSkipsSubagentAndAutopilotContinuationIdle(t *testing.T) { t.Run("with root reply", func(t *testing.T) { checkSendAndWaitSkipsSubagentAndAutopilotContinuationIdle(t, true) diff --git a/go/skill_provider_test.go b/go/skill_provider_test.go new file mode 100644 index 0000000000..957b99aa8f --- /dev/null +++ b/go/skill_provider_test.go @@ -0,0 +1,700 @@ +package copilot + +import ( + "context" + "encoding/json" + "errors" + "log" + "strings" + "sync" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +type testSkillProvider struct { + mu sync.Mutex + skills []rpc.SkillProviderDescriptor + markdown map[string]string + listErr error + readErr error + calls []string +} + +func (p *testSkillProvider) ListSkills(context.Context) ([]rpc.SkillProviderDescriptor, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "list") + if p.listErr != nil { + return nil, p.listErr + } + return p.skills, nil +} + +func (p *testSkillProvider) ReadSkill(_ context.Context, name string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.calls = append(p.calls, "read:"+name) + if p.readErr != nil { + return "", p.readErr + } + markdown, ok := p.markdown[name] + if !ok { + return "", ErrSkillNotFound + } + return markdown, nil +} + +func (p *testSkillProvider) snapshotCalls() []string { + p.mu.Lock() + defer p.mu.Unlock() + return append([]string(nil), p.calls...) +} + +var reviewSkillDescriptor = rpc.SkillProviderDescriptor{ + Name: "review", + Description: "Reviews code", +} + +func newTestSkillProvider() *testSkillProvider { + return &testSkillProvider{ + skills: []rpc.SkillProviderDescriptor{reviewSkillDescriptor}, + markdown: map[string]string{"review": "Review carefully."}, + } +} + +func TestSkillProviderSessionPayloads(t *testing.T) { + t.Run("omits flag without provider and sends flag with provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + createParams := make(chan json.RawMessage, 2) + resumeParams := make(chan json.RawMessage, 2) + 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.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{}); err != nil { + t.Fatalf("CreateSession without provider failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "resume-without-provider", &ResumeSessionConfig{}); err != nil { + t.Fatalf("ResumeSession without provider failed: %v", err) + } + provider := newTestSkillProvider() + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("CreateSession with provider failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "resume-with-provider", &ResumeSessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("ResumeSession with provider failed: %v", err) + } + + assertSkillProviderFlag(t, <-createParams, false) + assertSkillProviderFlag(t, <-resumeParams, false) + assertSkillProviderFlag(t, <-createParams, true) + assertSkillProviderFlag(t, <-resumeParams, true) + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called during open: %v", got) + } + }) + + t.Run("keeps empty mode enableSkills default with provider", func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + options: ClientOptions{Mode: ModeEmpty}, + } + + createParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + server.SetRequestHandler("session.options.update", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{}`), nil + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{ + AvailableTools: []string{}, + SkillProvider: newTestSkillProvider(), + }); err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(<-createParams, &payload); err != nil { + t.Fatal(err) + } + if payload["enableSkills"] != false { + t.Fatalf("enableSkills = %v, want false", payload["enableSkills"]) + } + if payload["hasSkillProvider"] != true { + t.Fatalf("hasSkillProvider = %v, want true", payload["hasSkillProvider"]) + } + }) +} + +func TestSkillProviderServesEarlyCallbacksDuringOpen(t *testing.T) { + for _, method := range []string{"session.create", "session.resume"} { + t.Run(method, func(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() + + earlyResult := make(chan rpc.SkillProviderListResult, 1) + server.SetRequestHandler(method, func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + raw, err := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": sessionID}) + if err != nil { + if rpcErr, ok := err.(*jsonrpc2.Error); ok { + return nil, rpcErr + } + return nil, &jsonrpc2.Error{Code: -32603, Message: err.Error()} + } + var result rpc.SkillProviderListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: err.Error()} + } + earlyResult <- result + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + + provider := newTestSkillProvider() + if method == "session.create" { + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + } else if _, err := client.ResumeSession(t.Context(), "early-resume", &ResumeSessionConfig{SkillProvider: provider}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + + got := <-earlyResult + if len(got.Skills) != 1 || got.Skills[0].Name != "review" { + t.Fatalf("early list result = %+v", got) + } + }) + } +} + +func TestSkillProviderRejectsCloudBeforeConnect(t *testing.T) { + provider := newTestSkillProvider() + client := &Client{} + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Cloud: &CloudSessionOptions{}, + SkillProvider: provider, + }) + + if err == nil || err.Error() != "Skill providers are not supported for cloud sessions." { + t.Fatalf("CreateSession error = %v", err) + } + if client.client != nil { + t.Fatal("client connected before rejecting cloud skill provider") + } + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called: %v", got) + } +} + +func TestSkillProviderDispatch(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() + + provider := &testSkillProvider{ + skills: []rpc.SkillProviderDescriptor{ + reviewSkillDescriptor, + { + Name: "deploy", + Description: "Deploys the service", + UserInvocable: Bool(false), + DisableModelInvocation: Bool(true), + ArgumentHint: String("[environment]"), + }, + }, + markdown: map[string]string{"deploy": "# deploy"}, + } + session := newSession("dispatch-session", rpcClient, "", false) + session.registerSkillProvider(provider) + client.sessions[session.SessionID] = session + + listRaw, rpcErr := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + if rpcErr != nil { + t.Fatalf("skillProvider.list failed: %v", rpcErr) + } + var listResult rpc.SkillProviderListResult + if err := json.Unmarshal(listRaw, &listResult); err != nil { + t.Fatal(err) + } + if len(listResult.Skills) != 2 || listResult.Skills[1].Name != "deploy" { + t.Fatalf("list result = %+v", listResult) + } + + readRaw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "deploy", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + var readResult rpc.SkillProviderReadResult + if err := json.Unmarshal(readRaw, &readResult); err != nil { + t.Fatal(err) + } + if readResult.Markdown == nil { + t.Fatal("markdown = nil, want # deploy") + } + if *readResult.Markdown != "# deploy" { + t.Fatalf("markdown = %q, want # deploy", *readResult.Markdown) + } + if got := provider.snapshotCalls(); strings.Join(got, ",") != "list,read:deploy" { + t.Fatalf("provider calls = %v", got) + } +} + +func TestSkillProviderOmitsUnsetOptionalDescriptorFields(t *testing.T) { + data, err := json.Marshal(rpc.SkillProviderListResult{ + Skills: []rpc.SkillProviderDescriptor{{Name: "review", Description: "Reviews code"}}, + }) + if err != nil { + t.Fatal(err) + } + var decoded map[string][]map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + skill := decoded["skills"][0] + for _, key := range []string{"argumentHint", "userInvocable", "disableModelInvocation"} { + if _, ok := skill[key]; ok { + t.Fatalf("%s should be omitted when unset: %s", key, data) + } + } +} + +func TestSkillProviderEmptyListBecomesEmptyArray(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() + session := newSession("empty-list-session", rpcClient, "", false) + session.registerSkillProvider(&testSkillProvider{}) + client.sessions[session.SessionID] = session + + raw, rpcErr := server.Request(t.Context(), "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + if rpcErr != nil { + t.Fatalf("skillProvider.list failed: %v", rpcErr) + } + if string(raw) != `{"skills":[]}` { + t.Fatalf("list response = %s, want empty skills array", raw) + } +} + +func TestSkillProviderErrorEnvelopes(t *testing.T) { + t.Run("read not found returns null markdown", func(t *testing.T) { + _, server, sessionID := skillProviderErrorHarness(t, newTestSkillProvider()) + raw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": sessionID, + "name": "missing", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + if string(raw) != `{"markdown":null}` { + t.Fatalf("read response = %s, want null markdown", raw) + } + var result rpc.SkillProviderReadResult + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatal(err) + } + if result.Markdown != nil { + t.Fatalf("markdown = %q, want nil", *result.Markdown) + } + }) + + for _, method := range []string{"skillProvider.list", "skillProvider.read"} { + t.Run(method+" provider failure", func(t *testing.T) { + output := captureSkillProviderLog(t) + secret := errors.New("db-password-in-error") + provider := newTestSkillProvider() + operation := "listSkills" + if method == "skillProvider.list" { + provider.listErr = secret + } else { + provider.readErr = secret + operation = "readSkill" + } + _, server, sessionID := skillProviderErrorHarness(t, provider) + params := map[string]any{"sessionId": sessionID} + wantMessage := "Skill provider " + operation + " failed" + if method == "skillProvider.read" { + params["name"] = "review" + } + + rpcErr := requestSkillProviderError(t, server, method, params) + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage { + t.Fatalf("error = %+v, want generic message %q", rpcErr, wantMessage) + } + if rpcErr.Data != nil { + t.Fatalf("generic provider error data = %s, want omitted", rpcErr.Data) + } + if strings.Contains(rpcErr.Message, secret.Error()) { + t.Fatalf("provider error leaked secret: %q", rpcErr.Message) + } + wantLog := "skill provider " + operation + " failed: session_id=" + sessionID + " error=" + secret.Error() + if got := output.text(); !strings.Contains(got, wantLog) { + t.Fatalf("log = %q, want it to contain %q", got, wantLog) + } + }) + } + + t.Run("not-found classification panic", func(t *testing.T) { + captureSkillProviderLog(t) + provider := newTestSkillProvider() + provider.readErr = panickyIsError{} + _, server, sessionID := skillProviderErrorHarness(t, provider) + + rpcErr := requestSkillProviderError(t, server, "skillProvider.read", map[string]any{ + "sessionId": sessionID, + "name": "review", + }) + if rpcErr.Code != -32603 || rpcErr.Message != "Skill provider readSkill failed" || rpcErr.Data != nil { + t.Fatalf("error = %+v, want generic readSkill failure", rpcErr) + } + }) + + t.Run("unknown session", func(t *testing.T) { + _, server, _ := skillProviderErrorHarness(t, newTestSkillProvider()) + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": "missing"}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: missing") + }) + + for _, method := range []string{"skillProvider.list", "skillProvider.read"} { + t.Run(method+" provider panic", func(t *testing.T) { + output := captureSkillProviderLog(t) + _, server, sessionID := skillProviderErrorHarness(t, panickingSkillProvider{}) + params := map[string]any{"sessionId": sessionID} + wantMessage := "Skill provider listSkills failed" + if method == "skillProvider.read" { + params["name"] = "review" + wantMessage = "Skill provider readSkill failed" + } + + rpcErr := requestSkillProviderError(t, server, method, params) + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage || rpcErr.Data != nil { + t.Fatalf("error = %+v, want generic message %q without data", rpcErr, wantMessage) + } + if got := output.text(); !strings.Contains(got, "panic=db-password-in-panic") { + t.Fatalf("log = %q, want the recovered panic value", got) + } + }) + } + + t.Run("no provider", func(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() + session := newSession("no-provider-session", rpcClient, "", false) + client.sessions[session.SessionID] = session + + rpcErr := requestSkillProviderError(t, server, "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "review", + }) + assertSkillProviderError(t, rpcErr, "No skill provider for session: no-provider-session") + }) +} + +func TestSkillProviderTeardown(t *testing.T) { + t.Run("disconnect clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "disconnect-session") + + if err := session.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: disconnect-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after disconnect: %v", got) + } + _ = client + }) + + t.Run("delete clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "delete-session") + server.SetRequestHandler("session.delete", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + if err := client.DeleteSession(t.Context(), session.SessionID); err != nil { + t.Fatalf("DeleteSession failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: delete-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after delete: %v", got) + } + }) + + t.Run("connection loss clears provider", func(t *testing.T) { + client, server, provider, session := newSkillProviderOpenSession(t, "closed-session") + + client.handleConnectionClose() + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: closed-session") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after connection loss: %v", got) + } + }) + + t.Run("failed create cleanup", func(t *testing.T) { + provider := newTestSkillProvider() + var sessionID string + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID = sessionIDFromParams(t, params) + return nil, &jsonrpc2.Error{Code: -32000, Message: "create failed"} + }) + + if _, err := client.CreateSession(t.Context(), &SessionConfig{SkillProvider: provider}); err == nil { + t.Fatal("CreateSession succeeded unexpectedly") + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": sessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: "+sessionID) + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after failed create: %v", got) + } + }) + + t.Run("failed resume cleanup", func(t *testing.T) { + provider := newTestSkillProvider() + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.resume", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "resume failed"} + }) + + if _, err := client.ResumeSession(t.Context(), "failed-resume", &ResumeSessionConfig{SkillProvider: provider}); err == nil { + t.Fatal("ResumeSession succeeded unexpectedly") + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": "failed-resume"}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: failed-resume") + if got := provider.snapshotCalls(); len(got) != 0 { + t.Fatalf("provider was called after failed resume: %v", got) + } + }) +} + +func TestSkillProviderResumeRebinding(t *testing.T) { + t.Run("serves replacement provider", func(t *testing.T) { + client, server := newSkillProviderClient(t) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + original := newTestSkillProvider() + replacement := newTestSkillProvider() + replacement.markdown = map[string]string{"review": "replacement"} + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: "rebind-session", + SkillProvider: original, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{SkillProvider: replacement}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + raw, rpcErr := server.Request(t.Context(), "skillProvider.read", map[string]any{ + "sessionId": session.SessionID, + "name": "review", + }) + if rpcErr != nil { + t.Fatalf("skillProvider.read failed: %v", rpcErr) + } + var result rpc.SkillProviderReadResult + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatal(err) + } + if result.Markdown == nil { + t.Fatal("markdown = nil, want replacement") + } + if *result.Markdown != "replacement" { + t.Fatalf("markdown = %q, want replacement", *result.Markdown) + } + if got := original.snapshotCalls(); len(got) != 0 { + t.Fatalf("original provider was called after resume: %v", got) + } + if got := replacement.snapshotCalls(); strings.Join(got, ",") != "read:review" { + t.Fatalf("replacement provider calls = %v", got) + } + }) + + t.Run("resume without provider unbinds previous provider", func(t *testing.T) { + client, server, original, session := newSkillProviderOpenSession(t, "unbind-session") + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `"}`), nil + }) + + if _, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{}); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + rpcErr := requestSkillProviderError(t, server, "skillProvider.list", map[string]any{"sessionId": session.SessionID}) + assertSkillProviderError(t, rpcErr, "No skill provider for session: unbind-session") + if got := original.snapshotCalls(); len(got) != 0 { + t.Fatalf("original provider was called after unbound resume: %v", got) + } + }) +} + +type panickingSkillProvider struct{} + +func (panickingSkillProvider) ListSkills(context.Context) ([]rpc.SkillProviderDescriptor, error) { + panic("db-password-in-panic") +} + +func (panickingSkillProvider) ReadSkill(context.Context, string) (string, error) { + panic("db-password-in-panic") +} + +// panickyIsError panics when errors.Is asks whether it matches a target. +type panickyIsError struct{} + +func (panickyIsError) Error() string { return "panicky" } + +func (panickyIsError) Is(error) bool { panic("classification panicked") } + +func captureSkillProviderLog(t *testing.T) *ahpTestLog { + t.Helper() + output := new(ahpTestLog) + previous := log.Writer() + log.SetOutput(output) + t.Cleanup(func() { log.SetOutput(previous) }) + return output +} + +func newSkillProviderClient(t *testing.T) (*Client, *jsonrpc2.Client) { + t.Helper() + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + client.setupNotificationHandler() + return client, server +} + +func newSkillProviderOpenSession(t *testing.T, sessionID string) (*Client, *jsonrpc2.Client, *testSkillProvider, *Session) { + t.Helper() + client, server := newSkillProviderClient(t) + provider := newTestSkillProvider() + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + gotSessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + gotSessionID + `"}`), nil + }) + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: sessionID, + SkillProvider: provider, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + return client, server, provider, session +} + +func skillProviderErrorHarness(t *testing.T, provider SkillProvider) (*Client, *jsonrpc2.Client, string) { + t.Helper() + client, server := newSkillProviderClient(t) + session := newSession("error-session", client.client, "", false) + session.registerSkillProvider(provider) + client.sessions[session.SessionID] = session + return client, server, session.SessionID +} + +func requestSkillProviderError(t *testing.T, server *jsonrpc2.Client, method string, params map[string]any) *jsonrpc2.Error { + t.Helper() + _, err := server.Request(t.Context(), method, params) + if err == nil { + t.Fatalf("%s succeeded unexpectedly", method) + } + rpcErr, ok := err.(*jsonrpc2.Error) + if !ok { + t.Fatalf("%s error = %T %v, want *jsonrpc2.Error", method, err, err) + } + return rpcErr +} + +func assertSkillProviderError(t *testing.T, rpcErr *jsonrpc2.Error, wantMessage string) { + t.Helper() + if rpcErr.Code != -32603 || rpcErr.Message != wantMessage { + t.Fatalf("error = %+v, want code -32603 message %q", rpcErr, wantMessage) + } + if rpcErr.Data != nil { + t.Fatalf("error data = %s, want omitted", rpcErr.Data) + } +} + +func assertSkillProviderFlag(t *testing.T, params json.RawMessage, wantPresent bool) { + t.Helper() + var payload map[string]any + if err := json.Unmarshal(params, &payload); err != nil { + t.Fatalf("failed to decode request params: %v", err) + } + got, present := payload["hasSkillProvider"] + if !wantPresent { + if present { + t.Fatalf("hasSkillProvider = %v, want omitted", got) + } + return + } + if got != true { + t.Fatalf("hasSkillProvider = %v, want true", got) + } + if _, present := payload["skillProvider"]; present { + t.Fatalf("skillProvider callback was serialized: %v", payload["skillProvider"]) + } +} diff --git a/go/subagent_hooks_test.go b/go/subagent_hooks_test.go new file mode 100644 index 0000000000..8f68cc49d6 --- /dev/null +++ b/go/subagent_hooks_test.go @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package copilot + +import ( + "encoding/json" + "reflect" + "testing" + "time" +) + +func TestSession_SubagentStartHook(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + session.SessionID = "parent-session" + + var captured SubagentStartHookInput + session.registerHooks(&SessionHooks{ + OnSubagentStart: func(input SubagentStartHookInput, invocation HookInvocation) (*SubagentStartHookOutput, error) { + captured = input + if invocation.SessionID != "parent-session" { + t.Errorf("invocation session ID = %q, want parent-session", invocation.SessionID) + } + return &SubagentStartHookOutput{AdditionalContext: "read only the requested file"}, nil + }, + }) + + raw := json.RawMessage(`{"sessionId":"parent-session","timestamp":1700000000123,"cwd":"C:\\work","transcriptPath":"C:\\transcript.jsonl","agentName":"explore","agentDisplayName":"Explore Agent","agentDescription":"Find files"}`) + output, err := session.handleHooksInvoke("subagentStart", raw) + if err != nil { + t.Fatalf("subagentStart dispatch failed: %v", err) + } + if captured.SessionID != "parent-session" || !captured.Timestamp.Equal(time.UnixMilli(1700000000123)) || + captured.WorkingDirectory != `C:\work` || captured.TranscriptPath != `C:\transcript.jsonl` || + captured.AgentName != "explore" || captured.AgentDisplayName != "Explore Agent" || + captured.AgentDescription != "Find files" { + t.Errorf("subagentStart input = %+v", captured) + } + assertHookJSON(t, captured, raw) + assertHookJSON(t, output, json.RawMessage(`{"additionalContext":"read only the requested file"}`)) + + session.registerHooks(&SessionHooks{}) + output, err = session.handleHooksInvoke("subagentStart", raw) + if err != nil || output != nil { + t.Errorf("unregistered subagentStart = (%v, %v), want (nil, nil)", output, err) + } +} + +func TestSession_SubagentStopHook(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + session.SessionID = "parent-session" + + raw := json.RawMessage(`{"sessionId":"parent-session","timestamp":1700000000456,"cwd":"C:\\work","transcriptPath":"C:\\transcript.jsonl","agentId":"read-file","agentType":"explore","agentName":"explore","agentDisplayName":"Explore Agent","agentDescription":"Find files","stopReason":"end_turn","response":"Hello from subagent test!"}`) + tests := []struct { + name string + output *SubagentStopHookOutput + want json.RawMessage + }{ + { + name: "block with a reason", + output: &SubagentStopHookOutput{Decision: "block", Reason: "Read the file again"}, + want: json.RawMessage(`{"decision":"block","reason":"Read the file again"}`), + }, + { + name: "replace the response", + output: &SubagentStopHookOutput{ModifiedResponse: String("Changed result")}, + want: json.RawMessage(`{"modifiedResponse":"Changed result"}`), + }, + { + name: "replace with an empty response", + output: &SubagentStopHookOutput{ModifiedResponse: String("")}, + want: json.RawMessage(`{"modifiedResponse":""}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var captured SubagentStopHookInput + session.registerHooks(&SessionHooks{ + OnSubagentStop: func(input SubagentStopHookInput, invocation HookInvocation) (*SubagentStopHookOutput, error) { + captured = input + if invocation.SessionID != "parent-session" { + t.Errorf("invocation session ID = %q, want parent-session", invocation.SessionID) + } + return tt.output, nil + }, + }) + + output, err := session.handleHooksInvoke("subagentStop", raw) + if err != nil { + t.Fatalf("subagentStop dispatch failed: %v", err) + } + if captured.SessionID != "parent-session" || !captured.Timestamp.Equal(time.UnixMilli(1700000000456)) || + captured.WorkingDirectory != `C:\work` || captured.TranscriptPath != `C:\transcript.jsonl` || + captured.AgentID != "read-file" || captured.AgentType != "explore" || + captured.AgentName != "explore" || captured.AgentDisplayName != "Explore Agent" || + captured.AgentDescription != "Find files" || captured.StopReason != "end_turn" || + captured.Response != "Hello from subagent test!" { + t.Errorf("subagentStop input = %+v", captured) + } + assertHookJSON(t, captured, raw) + assertHookJSON(t, output, tt.want) + }) + } + + session.registerHooks(&SessionHooks{}) + output, err := session.handleHooksInvoke("subagentStop", raw) + if err != nil || output != nil { + t.Errorf("unregistered subagentStop = (%v, %v), want (nil, nil)", output, err) + } +} + +func assertHookJSON(t *testing.T, value any, expected json.RawMessage) { + t.Helper() + gotJSON, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal %T: %v", value, err) + } + var got, want any + if err := json.Unmarshal(gotJSON, &got); err != nil { + t.Fatalf("decode marshaled %T: %v", value, err) + } + if err := json.Unmarshal(expected, &want); err != nil { + t.Fatalf("decode expected JSON: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("JSON for %T = %s, want %s", value, gotJSON, expected) + } +} diff --git a/go/types.go b/go/types.go index ff59dbd934..dc478bbdbd 100644 --- a/go/types.go +++ b/go/types.go @@ -3,6 +3,7 @@ package copilot import ( "context" "encoding/json" + "errors" "time" "github.com/github/copilot-sdk/go/rpc" @@ -373,12 +374,14 @@ const ( SectionToolInstructions = "tool_instructions" // SectionCustomInstructions covers repository and organization custom instructions. SectionCustomInstructions = "custom_instructions" - // SectionRuntimeInstructions targets runtime-provided context and instructions - // (e.g. system notifications, memories, workspace context, mode-specific instructions, - // content-exclusion policy). + // SectionRuntimeInstructions targets runtime-provided system-prompt context and + // instructions, such as system notifications, memories, workspace context, and + // content-exclusion policy. Mode-specific instructions can travel in transition + // messages instead. SectionRuntimeInstructions = "runtime_instructions" // SectionLastInstructions covers end-of-prompt instructions: parallel tool calling, - // persistence, and task completion. + // persistence, task completion, and configured subagent-model guidance when the + // task tool is available. SectionLastInstructions = "last_instructions" ) @@ -1027,6 +1030,98 @@ type AgentStopHookOutput struct { // AgentStopHandler handles agent-stop hook invocations. type AgentStopHandler func(input AgentStopHookInput, invocation HookInvocation) (*AgentStopHookOutput, error) +// SubagentStartHookInput is the input before a subagent's first turn. +type SubagentStartHookInput struct { + SessionID string `json:"sessionId"` + Timestamp time.Time `json:"-"` + WorkingDirectory string `json:"cwd"` + TranscriptPath string `json:"transcriptPath"` + AgentName string `json:"agentName"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AgentDescription string `json:"agentDescription,omitempty"` +} + +// MarshalJSON implements json.Marshaler, emitting Timestamp as Unix milliseconds. +func (h SubagentStartHookInput) MarshalJSON() ([]byte, error) { + type alias SubagentStartHookInput + return json.Marshal(&struct { + Timestamp int64 `json:"timestamp"` + alias + }{Timestamp: h.Timestamp.UnixMilli(), alias: alias(h)}) +} + +// UnmarshalJSON implements json.Unmarshaler, parsing Timestamp from Unix milliseconds. +func (h *SubagentStartHookInput) UnmarshalJSON(data []byte) error { + type alias SubagentStartHookInput + aux := &struct { + Timestamp int64 `json:"timestamp"` + *alias + }{alias: (*alias)(h)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + h.Timestamp = time.UnixMilli(aux.Timestamp) + return nil +} + +// SubagentStartHookOutput provides context prepended to the subagent's initial prompt. +type SubagentStartHookOutput struct { + AdditionalContext string `json:"additionalContext,omitempty"` +} + +// SubagentStartHandler handles subagent-start hook invocations. +type SubagentStartHandler func(input SubagentStartHookInput, invocation HookInvocation) (*SubagentStartHookOutput, error) + +// SubagentStopHookInput is the input after a subagent completes a turn. +type SubagentStopHookInput struct { + SessionID string `json:"sessionId"` + Timestamp time.Time `json:"-"` + WorkingDirectory string `json:"cwd"` + TranscriptPath string `json:"transcriptPath"` + AgentID string `json:"agentId,omitempty"` + AgentType string `json:"agentType"` + AgentName string `json:"agentName"` + AgentDisplayName string `json:"agentDisplayName,omitempty"` + AgentDescription string `json:"agentDescription,omitempty"` + StopReason string `json:"stopReason"` + Response string `json:"response"` +} + +// MarshalJSON implements json.Marshaler, emitting Timestamp as Unix milliseconds. +func (h SubagentStopHookInput) MarshalJSON() ([]byte, error) { + type alias SubagentStopHookInput + return json.Marshal(&struct { + Timestamp int64 `json:"timestamp"` + alias + }{Timestamp: h.Timestamp.UnixMilli(), alias: alias(h)}) +} + +// UnmarshalJSON implements json.Unmarshaler, parsing Timestamp from Unix milliseconds. +func (h *SubagentStopHookInput) UnmarshalJSON(data []byte) error { + type alias SubagentStopHookInput + aux := &struct { + Timestamp int64 `json:"timestamp"` + *alias + }{alias: (*alias)(h)} + if err := json.Unmarshal(data, aux); err != nil { + return err + } + h.Timestamp = time.UnixMilli(aux.Timestamp) + return nil +} + +// SubagentStopHookOutput can block a subagent's completion or replace its response. +// Decision must be "block" with a nonempty Reason, "allow", or empty to allow. +// Reason without a block decision and unsupported decisions fail the subagent. +type SubagentStopHookOutput struct { + Decision string `json:"decision,omitempty"` + Reason string `json:"reason,omitempty"` + ModifiedResponse *string `json:"modifiedResponse,omitempty"` +} + +// SubagentStopHandler handles subagent-stop hook invocations. +type SubagentStopHandler func(input SubagentStopHookInput, invocation HookInvocation) (*SubagentStopHookOutput, error) + // PreMCPToolCallHookInput is the input for a pre-mcp-tool-call hook type PreMCPToolCallHookInput struct { SessionID string `json:"sessionId"` @@ -1086,6 +1181,8 @@ type SessionHooks struct { OnSessionEnd SessionEndHandler OnErrorOccurred ErrorOccurredHandler OnAgentStop AgentStopHandler + OnSubagentStart SubagentStartHandler + OnSubagentStop SubagentStopHandler OnPreMCPToolCall PreMCPToolCallHandler } @@ -1242,6 +1339,8 @@ type ToolSearchConfig struct { type SessionFSCapabilities struct { // Sqlite indicates whether the provider supports SQLite query/exists operations. Sqlite bool + // Binary indicates whether the provider supports exact binary file reads and writes. + Binary bool } // SessionFSConfig configures a custom session filesystem provider. @@ -1346,6 +1445,24 @@ const ( AskUserVariantElicitation AskUserVariant = "elicitation" ) +// ErrSkillNotFound is returned by [SkillProvider.ReadSkill] when a listed skill +// is no longer available. It can be wrapped; the SDK checks it with [errors.Is]. +var ErrSkillNotFound = errors.New("skill not found") + +// SkillProvider supplies session-scoped skills from the SDK host. +// +// Experimental: this API may change or be removed in future SDK or CLI +// releases. Implementations must be safe for concurrent calls. +type SkillProvider interface { + // ListSkills returns catalog metadata for skills provided by this session. + // Return nil, nil to publish an empty catalog. + ListSkills(ctx context.Context) ([]rpc.SkillProviderDescriptor, error) + // ReadSkill returns the SKILL.md markdown for name. Return an error wrapping + // [ErrSkillNotFound] when the skill no longer exists. The markdown is + // ignored whenever a non-nil error is returned. + ReadSkill(ctx context.Context, name string) (string, error) +} + // SessionConfig configures a new session type SessionConfig struct { // SessionID is an optional custom session ID @@ -1545,6 +1662,12 @@ type SessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // SkillProvider supplies ephemeral SDK-hosted skills for this session. The + // provider is never serialized or persisted; re-supply it when resuming. + // + // Experimental: this API may change or be removed in future SDK or CLI + // releases. Implementations must be safe for concurrent calls. + SkillProvider SkillProvider `json:"-"` // PluginDirectories is a list of local filesystem paths to Open Plugins-format // directories (https://open-plugins.com/) to load for this session. // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). @@ -1730,6 +1853,11 @@ type ManagedSettingsPermissions struct { // Allow lists operations permitted without prompting. Every declared allow // list across managed layers must admit an operation for it to be allowed. Allow []string `json:"allow,omitzero"` + // LimitTo is a closed-world host boundary expressed as Domain(hostname), + // Domain(IP), or Domain(*.example.com) rules. Schemes, ports, paths, + // queries, and fragments are rejected. Multiple managed layers intersect + // their lists. A present empty list denies all hosts. + LimitTo []string `json:"limitTo,omitzero"` } // ToolDefer controls whether a tool may be deferred (loaded lazily via tool @@ -2110,6 +2238,13 @@ type ResumeSessionConfig struct { Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string + // SkillProvider supplies ephemeral SDK-hosted skills for this resumed + // session. Re-supply it on every resume; omitting it unbinds any previous + // provider. + // + // Experimental: this API may change or be removed in future SDK or CLI + // releases. Implementations must be safe for concurrent calls. + SkillProvider SkillProvider `json:"-"` // PluginDirectories is a list of local filesystem paths to Open Plugins-format // directories (https://open-plugins.com/) to load for this session. // Relative paths resolve against WorkingDirectory (or the runtime cwd if unset). @@ -2150,13 +2285,18 @@ type ResumeSessionConfig struct { // ContinuePendingWork, when non-nil, controls whether the runtime continues any // tool calls or permission prompts that were still pending when the session was // last suspended. Nil leaves the runtime default unchanged; use Bool(false) to - // explicitly treat pending work as interrupted on resume. + // explicitly treat pending work as interrupted on resume. Completed tool results + // already durably recorded by the runtime are preserved. // // For permission requests, the runtime re-emits permission.requested so the // registered OnPermissionRequest handler can re-prompt; for external tool calls, // the consumer is expected to supply the result via the corresponding low-level // RPC method. ContinuePendingWork *bool + // AllowTranscriptRecovery controls whether resume repairs a damaged transcript. + // Nil uses the runtime default (true) in all modes. Set false to reject recovery. + // Recovery may discard a torn tail; inspect Session.TranscriptRecovery(). + AllowTranscriptRecovery *bool // OnEvent is an optional event handler registered before the session.resume RPC // is issued, ensuring early events are delivered. See SessionConfig.OnEvent. OnEvent SessionEventHandler @@ -2272,6 +2412,10 @@ type ProviderConfig struct { // Set "websockets" to deliver Responses API requests over a persistent WebSocket // connection instead of HTTP. Applies to OpenAI-compatible providers using WireAPI "responses". Transport string `json:"transport,omitempty"` + // ModelProvider is the product serving the model, reported in telemetry as + // model_provider. Allowed values are "openai", "anthropic", "azure_openai", + // "ollama", "lm_studio", "foundry_local", and "llama_cpp"; only affects telemetry. + ModelProvider string `json:"modelProvider,omitempty"` // BaseURL is the API endpoint URL BaseURL string `json:"baseUrl"` // APIKey is the API key. Optional for local providers like Ollama. @@ -2400,6 +2544,11 @@ type NamedProviderConfig struct { Type string `json:"type,omitempty"` // WireAPI is the API format (openai/azure only): "completions" or "responses". Defaults to "completions". WireAPI string `json:"wireApi,omitempty"` + // ModelProvider is the product serving this provider's models, reported in + // telemetry as model_provider. Allowed values are "openai", "anthropic", + // "azure_openai", "ollama", "lm_studio", "foundry_local", and "llama_cpp"; + // only affects telemetry. + ModelProvider string `json:"modelProvider,omitempty"` // BaseURL is the API endpoint URL. BaseURL string `json:"baseUrl"` // APIKey is the API key. Optional for local providers like Ollama. @@ -2714,6 +2863,7 @@ type createSessionRequest struct { EnableHostGitOperations *bool `json:"enableHostGitOperations,omitempty"` EnableSessionStore *bool `json:"enableSessionStore,omitempty"` EnableSkills *bool `json:"enableSkills,omitempty"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` SkillDirectories []string `json:"skillDirectories,omitempty"` PluginDirectories []string `json:"pluginDirectories,omitempty"` InstructionDirectories []string `json:"instructionDirectories,omitempty"` @@ -2804,8 +2954,10 @@ type resumeSessionRequest struct { EnableHostGitOperations *bool `json:"enableHostGitOperations,omitempty"` EnableSessionStore *bool `json:"enableSessionStore,omitempty"` EnableSkills *bool `json:"enableSkills,omitempty"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` DisableResume *bool `json:"disableResume,omitempty"` ContinuePendingWork *bool `json:"continuePendingWork,omitempty"` + AllowTranscriptRecovery *bool `json:"allowTranscriptRecovery,omitempty"` Streaming *bool `json:"streaming,omitempty"` IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` @@ -2850,10 +3002,18 @@ type resumeSessionRequest struct { // resumeSessionResponse is the response from session.resume type resumeSessionResponse struct { - SessionID string `json:"sessionId"` - WorkspacePath string `json:"workspacePath"` - Capabilities *SessionCapabilities `json:"capabilities,omitempty"` - OpenCanvases []rpc.OpenCanvasInstance `json:"openCanvases,omitempty"` + SessionID string `json:"sessionId"` + WorkspacePath string `json:"workspacePath"` + Capabilities *SessionCapabilities `json:"capabilities,omitempty"` + OpenCanvases []rpc.OpenCanvasInstance `json:"openCanvases,omitempty"` + TranscriptRecovery *TranscriptRecoveryReport `json:"transcriptRecovery,omitempty"` +} + +// TranscriptRecoveryReport describes the repair performed while resuming a session. +type TranscriptRecoveryReport struct { + PlannedBackupPath string `json:"plannedBackupPath"` + InvalidLineNumbers []int `json:"invalidLineNumbers"` + SessionStartMoved bool `json:"sessionStartMoved"` } type hooksInvokeRequest struct { diff --git a/go/types_test.go b/go/types_test.go index ad7972fe15..7c9afb6274 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -124,6 +124,7 @@ func TestProviderConfig_JSONIncludesAllFields(t *testing.T) { Headers: map[string]string{"Authorization": "Bearer provider-token"}, ModelID: "gpt-4o", WireModel: "my-finetune-v3", + ModelProvider: "lm_studio", MaxPromptTokens: 100000, MaxOutputTokens: 4096, } @@ -147,6 +148,9 @@ func TestProviderConfig_JSONIncludesAllFields(t *testing.T) { if decoded["wireModel"] != "my-finetune-v3" { t.Errorf("expected wireModel 'my-finetune-v3', got %v", decoded["wireModel"]) } + if decoded["modelProvider"] != "lm_studio" { + t.Errorf("expected modelProvider 'lm_studio', got %v", decoded["modelProvider"]) + } if decoded["maxPromptTokens"] != float64(100000) { t.Errorf("expected maxPromptTokens 100000, got %v", decoded["maxPromptTokens"]) } @@ -175,13 +179,36 @@ func TestProviderConfig_JSONOmitsUnsetTokenFields(t *testing.T) { t.Fatalf("failed to unmarshal ProviderConfig: %v", err) } - for _, field := range []string{"modelId", "wireModel", "maxPromptTokens", "maxOutputTokens", "headers"} { + for _, field := range []string{"modelId", "wireModel", "modelProvider", "maxPromptTokens", "maxOutputTokens", "headers"} { if _, present := decoded[field]; present { t.Errorf("expected %q to be omitted when unset, got %v", field, decoded[field]) } } } +func TestNamedProviderConfig_JSONIncludesModelProvider(t *testing.T) { + cfg := NamedProviderConfig{ + Name: "local", + Type: "openai", + BaseURL: "http://localhost:11434/v1", + ModelProvider: "ollama", + } + + data, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("failed to marshal NamedProviderConfig: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to unmarshal NamedProviderConfig: %v", err) + } + + if decoded["modelProvider"] != "ollama" { + t.Errorf("expected modelProvider 'ollama', got %v", decoded["modelProvider"]) + } +} + func TestCustomAgentConfig_JSONIncludesModel(t *testing.T) { cfg := CustomAgentConfig{ Name: "model-agent", diff --git a/go/zsession_events.go b/go/zsession_events.go index d91f36537d..9887c2166b 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -143,6 +143,13 @@ type ( HookEndError = rpc.HookEndError HookProgressData = rpc.HookProgressData HookStartData = rpc.HookStartData + HumanResponseActor = rpc.HumanResponseActor + HumanResponseRecordedData = rpc.HumanResponseRecordedData + HumanResponseRecordedResponse = rpc.HumanResponseRecordedResponse + HumanResponseRecordedResponseAskUser = rpc.HumanResponseRecordedResponseAskUser + HumanResponseRecordedResponseExitPlanMode = rpc.HumanResponseRecordedResponseExitPlanMode + HumanResponseRecordedResponseResponseKind = rpc.HumanResponseRecordedResponseResponseKind + HumanResponseRecordedResponseUserInput = rpc.HumanResponseRecordedResponseUserInput IndexedSearchState = rpc.IndexedSearchState ManagedSettingsEnforcedAction = rpc.ManagedSettingsEnforcedAction ManagedSettingsEnforcedEscalation = rpc.ManagedSettingsEnforcedEscalation @@ -273,6 +280,7 @@ type ( PossibleURL = rpc.PossibleURL PromptCacheBreakData = rpc.PromptCacheBreakData RawCitationLocation = rpc.RawCitationLocation + RawHumanResponseRecordedResponse = rpc.RawHumanResponseRecordedResponse RawPermissionPromptRequest = rpc.RawPermissionPromptRequest RawPermissionRequest = rpc.RawPermissionRequest RawPermissionResult = rpc.RawPermissionResult @@ -422,6 +430,8 @@ type ( ToolExecutionCompleteContentType = rpc.ToolExecutionCompleteContentType ToolExecutionCompleteData = rpc.ToolExecutionCompleteData ToolExecutionCompleteError = rpc.ToolExecutionCompleteError + ToolExecutionCompleteFileEdit = rpc.ToolExecutionCompleteFileEdit + ToolExecutionCompleteFileEditKind = rpc.ToolExecutionCompleteFileEditKind ToolExecutionCompleteResult = rpc.ToolExecutionCompleteResult ToolExecutionCompleteShellExecution = rpc.ToolExecutionCompleteShellExecution ToolExecutionCompleteToolDescription = rpc.ToolExecutionCompleteToolDescription @@ -437,45 +447,48 @@ type ( ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone - ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData - ToolExecutionProgressData = rpc.ToolExecutionProgressData - ToolExecutionStartData = rpc.ToolExecutionStartData - ToolExecutionStartShellToolInfo = rpc.ToolExecutionStartShellToolInfo - ToolExecutionStartToolDescription = rpc.ToolExecutionStartToolDescription - ToolExecutionStartToolDescriptionMeta = rpc.ToolExecutionStartToolDescriptionMeta - ToolExecutionStartToolDescriptionMetaUI = rpc.ToolExecutionStartToolDescriptionMetaUI - ToolExecutionStartToolDescriptionMetaUIVisibility = rpc.ToolExecutionStartToolDescriptionMetaUIVisibility - ToolSearchActivatedData = rpc.ToolSearchActivatedData - ToolUserRequestedData = rpc.ToolUserRequestedData - UIEphemeralQueryData = rpc.UIEphemeralQueryData - UIEphemeralQueryPhase = rpc.UIEphemeralQueryPhase - UserInputCompletedData = rpc.UserInputCompletedData - UserInputRequestedData = rpc.UserInputRequestedData - UserMessageAgentMode = rpc.UserMessageAgentMode - UserMessageData = rpc.UserMessageData - UserMessageDelivery = rpc.UserMessageDelivery - UserToolSessionApproval = rpc.UserToolSessionApproval - UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands - UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool - UserToolSessionApprovalExtensionEnvAccess = rpc.UserToolSessionApprovalExtensionEnvAccess - UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement - UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess - UserToolSessionApprovalKind = rpc.UserToolSessionApprovalKind - UserToolSessionApprovalMCP = rpc.UserToolSessionApprovalMCP - UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory - UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead - UserToolSessionApprovalWorkflow = rpc.UserToolSessionApprovalWorkflow - UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite - Verbosity = rpc.Verbosity - WorkflowPermissionOperation = rpc.WorkflowPermissionOperation - WorkflowPermissionPhase = rpc.WorkflowPermissionPhase - WorkflowRunSettledData = rpc.WorkflowRunSettledData - WorkflowRunSettledStatus = rpc.WorkflowRunSettledStatus - WorkflowRunStartedData = rpc.WorkflowRunStartedData - WorkflowRunUpdatedData = rpc.WorkflowRunUpdatedData - WorkingDirectoryContext = rpc.WorkingDirectoryContext - WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType - WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation + // Deprecated: ToolExecutionPartialResultData is deprecated. + ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData + ToolExecutionProgressData = rpc.ToolExecutionProgressData + ToolExecutionStartData = rpc.ToolExecutionStartData + ToolExecutionStartShellToolInfo = rpc.ToolExecutionStartShellToolInfo + ToolExecutionStartToolDescription = rpc.ToolExecutionStartToolDescription + ToolExecutionStartToolDescriptionMeta = rpc.ToolExecutionStartToolDescriptionMeta + ToolExecutionStartToolDescriptionMetaUI = rpc.ToolExecutionStartToolDescriptionMetaUI + ToolExecutionStartToolDescriptionMetaUIVisibility = rpc.ToolExecutionStartToolDescriptionMetaUIVisibility + ToolSearchActivatedData = rpc.ToolSearchActivatedData + ToolShellOutputData = rpc.ToolShellOutputData + ToolShellOutputStream = rpc.ToolShellOutputStream + ToolUserRequestedData = rpc.ToolUserRequestedData + UIEphemeralQueryData = rpc.UIEphemeralQueryData + UIEphemeralQueryPhase = rpc.UIEphemeralQueryPhase + UserInputCompletedData = rpc.UserInputCompletedData + UserInputRequestedData = rpc.UserInputRequestedData + UserMessageAgentMode = rpc.UserMessageAgentMode + UserMessageData = rpc.UserMessageData + UserMessageDelivery = rpc.UserMessageDelivery + UserToolSessionApproval = rpc.UserToolSessionApproval + UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands + UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool + UserToolSessionApprovalExtensionEnvAccess = rpc.UserToolSessionApprovalExtensionEnvAccess + UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement + UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess + UserToolSessionApprovalKind = rpc.UserToolSessionApprovalKind + UserToolSessionApprovalMCP = rpc.UserToolSessionApprovalMCP + UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory + UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead + UserToolSessionApprovalWorkflow = rpc.UserToolSessionApprovalWorkflow + UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite + Verbosity = rpc.Verbosity + WorkflowPermissionOperation = rpc.WorkflowPermissionOperation + WorkflowPermissionPhase = rpc.WorkflowPermissionPhase + WorkflowRunSettledData = rpc.WorkflowRunSettledData + WorkflowRunSettledStatus = rpc.WorkflowRunSettledStatus + WorkflowRunStartedData = rpc.WorkflowRunStartedData + WorkflowRunUpdatedData = rpc.WorkflowRunUpdatedData + WorkingDirectoryContext = rpc.WorkingDirectoryContext + WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType + WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation ) // Session-event constants are generated in the rpc package and re-exported here for source compatibility. @@ -616,6 +629,12 @@ const ( FusionTurnKindUser = rpc.FusionTurnKindUser HandoffSourceTypeLocal = rpc.HandoffSourceTypeLocal HandoffSourceTypeRemote = rpc.HandoffSourceTypeRemote + HumanResponseActorHostAutomation = rpc.HumanResponseActorHostAutomation + HumanResponseActorHumanResponse = rpc.HumanResponseActorHumanResponse + HumanResponseActorUnknown = rpc.HumanResponseActorUnknown + HumanResponseRecordedResponseResponseKindAskUser = rpc.HumanResponseRecordedResponseResponseKindAskUser + HumanResponseRecordedResponseResponseKindExitPlanMode = rpc.HumanResponseRecordedResponseResponseKindExitPlanMode + HumanResponseRecordedResponseResponseKindUserInput = rpc.HumanResponseRecordedResponseResponseKindUserInput IndexedSearchStateDisabled = rpc.IndexedSearchStateDisabled IndexedSearchStateEnabled = rpc.IndexedSearchStateEnabled IndexedSearchStateFailed = rpc.IndexedSearchStateFailed @@ -648,6 +667,7 @@ const ( MCPOauthRequestReasonRefresh = rpc.MCPOauthRequestReasonRefresh MCPOauthRequestReasonUpscope = rpc.MCPOauthRequestReasonUpscope MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials = rpc.MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials + MCPServerSourceAccount = rpc.MCPServerSourceAccount MCPServerSourceBuiltin = rpc.MCPServerSourceBuiltin MCPServerSourceManaged = rpc.MCPServerSourceManaged MCPServerSourcePlugin = rpc.MCPServerSourcePlugin @@ -717,6 +737,10 @@ const ( PermissionApprovalEvaluationReasonCodeActionTooLong = rpc.PermissionApprovalEvaluationReasonCodeActionTooLong PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable = rpc.PermissionApprovalEvaluationReasonCodeArgumentBindingUnreviewable PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete = rpc.PermissionApprovalEvaluationReasonCodeAuthorizationHistoryIncomplete + PermissionApprovalEvaluationReasonCodeContentExcluded = rpc.PermissionApprovalEvaluationReasonCodeContentExcluded + PermissionApprovalEvaluationReasonCodeDynamicSource = rpc.PermissionApprovalEvaluationReasonCodeDynamicSource + PermissionApprovalEvaluationReasonCodeExecutableTooLarge = rpc.PermissionApprovalEvaluationReasonCodeExecutableTooLarge + PermissionApprovalEvaluationReasonCodeExecutableUnavailable = rpc.PermissionApprovalEvaluationReasonCodeExecutableUnavailable PermissionApprovalEvaluationReasonCodeInactive = rpc.PermissionApprovalEvaluationReasonCodeInactive PermissionApprovalEvaluationReasonCodeInherited = rpc.PermissionApprovalEvaluationReasonCodeInherited PermissionApprovalEvaluationReasonCodeInterpreterTooLarge = rpc.PermissionApprovalEvaluationReasonCodeInterpreterTooLarge @@ -735,11 +759,14 @@ const ( PermissionApprovalEvaluationReasonCodeSandboxBypass = rpc.PermissionApprovalEvaluationReasonCodeSandboxBypass PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable = rpc.PermissionApprovalEvaluationReasonCodeShellEnvironmentUnreviewable PermissionApprovalEvaluationReasonCodeTooLarge = rpc.PermissionApprovalEvaluationReasonCodeTooLarge + PermissionApprovalEvaluationReasonCodeTooManySources = rpc.PermissionApprovalEvaluationReasonCodeTooManySources PermissionApprovalEvaluationReasonCodeUnavailable = rpc.PermissionApprovalEvaluationReasonCodeUnavailable PermissionApprovalEvaluationReasonCodeUnknown = rpc.PermissionApprovalEvaluationReasonCodeUnknown PermissionApprovalEvaluationReasonCodeUnreadable = rpc.PermissionApprovalEvaluationReasonCodeUnreadable PermissionApprovalEvaluationReasonCodeUnrepresentablePath = rpc.PermissionApprovalEvaluationReasonCodeUnrepresentablePath PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation = rpc.PermissionApprovalEvaluationReasonCodeUnreviewableScriptInvocation + PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape = rpc.PermissionApprovalEvaluationReasonCodeUnsupportedCommandShape + PermissionApprovalEvaluationReasonCodeUnsupportedSource = rpc.PermissionApprovalEvaluationReasonCodeUnsupportedSource PermissionDecisionSourceAuthorizationCarryForward = rpc.PermissionDecisionSourceAuthorizationCarryForward PermissionMessageAuthorizationPolarityDenial = rpc.PermissionMessageAuthorizationPolarityDenial PermissionMessageAuthorizationPolarityGrant = rpc.PermissionMessageAuthorizationPolarityGrant @@ -878,6 +905,7 @@ const ( SessionEventTypeHookEnd = rpc.SessionEventTypeHookEnd SessionEventTypeHookProgress = rpc.SessionEventTypeHookProgress SessionEventTypeHookStart = rpc.SessionEventTypeHookStart + SessionEventTypeHumanResponseRecorded = rpc.SessionEventTypeHumanResponseRecorded SessionEventTypeMCPAppToolCallComplete = rpc.SessionEventTypeMCPAppToolCallComplete SessionEventTypeMCPHeadersRefreshCompleted = rpc.SessionEventTypeMCPHeadersRefreshCompleted SessionEventTypeMCPHeadersRefreshRequired = rpc.SessionEventTypeMCPHeadersRefreshRequired @@ -980,119 +1008,127 @@ const ( SessionEventTypeSystemMessage = rpc.SessionEventTypeSystemMessage SessionEventTypeSystemNotification = rpc.SessionEventTypeSystemNotification SessionEventTypeToolExecutionComplete = rpc.SessionEventTypeToolExecutionComplete - SessionEventTypeToolExecutionPartialResult = rpc.SessionEventTypeToolExecutionPartialResult - SessionEventTypeToolExecutionProgress = rpc.SessionEventTypeToolExecutionProgress - SessionEventTypeToolExecutionStart = rpc.SessionEventTypeToolExecutionStart - SessionEventTypeToolSearchActivated = rpc.SessionEventTypeToolSearchActivated - SessionEventTypeToolUserRequested = rpc.SessionEventTypeToolUserRequested - SessionEventTypeUIEphemeralQuery = rpc.SessionEventTypeUIEphemeralQuery - SessionEventTypeUserInputCompleted = rpc.SessionEventTypeUserInputCompleted - SessionEventTypeUserInputRequested = rpc.SessionEventTypeUserInputRequested - SessionEventTypeUserMessage = rpc.SessionEventTypeUserMessage - SessionEventTypeWorkflowRunSettled = rpc.SessionEventTypeWorkflowRunSettled - SessionEventTypeWorkflowRunStarted = rpc.SessionEventTypeWorkflowRunStarted - SessionEventTypeWorkflowRunUpdated = rpc.SessionEventTypeWorkflowRunUpdated - SessionLimitsExhaustedResponseActionAdd = rpc.SessionLimitsExhaustedResponseActionAdd - SessionLimitsExhaustedResponseActionCancel = rpc.SessionLimitsExhaustedResponseActionCancel - SessionLimitsExhaustedResponseActionSet = rpc.SessionLimitsExhaustedResponseActionSet - SessionLimitsExhaustedResponseActionUnset = rpc.SessionLimitsExhaustedResponseActionUnset - SessionModeAutopilot = rpc.SessionModeAutopilot - SessionModeInteractive = rpc.SessionModeInteractive - SessionModePlan = rpc.SessionModePlan - ShutdownTypeError = rpc.ShutdownTypeError - ShutdownTypeRoutine = rpc.ShutdownTypeRoutine - SkillInvokedTriggerAgentInvoked = rpc.SkillInvokedTriggerAgentInvoked - SkillInvokedTriggerContextLoad = rpc.SkillInvokedTriggerContextLoad - SkillInvokedTriggerUserInvoked = rpc.SkillInvokedTriggerUserInvoked - SkillSourceBuiltin = rpc.SkillSourceBuiltin - SkillSourceCustom = rpc.SkillSourceCustom - SkillSourceInherited = rpc.SkillSourceInherited - SkillSourcePersonalAgents = rpc.SkillSourcePersonalAgents - SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot - SkillSourcePlugin = rpc.SkillSourcePlugin - SkillSourceProject = rpc.SkillSourceProject - SkillSourceSDK = rpc.SkillSourceSDK - SubagentModelSelectionSourceAgentDefinitionDefault = rpc.SubagentModelSelectionSourceAgentDefinitionDefault - SubagentModelSelectionSourceComplementaryDefault = rpc.SubagentModelSelectionSourceComplementaryDefault - SubagentModelSelectionSourceConfiguredPreference = rpc.SubagentModelSelectionSourceConfiguredPreference - SubagentModelSelectionSourceConfiguredRequired = rpc.SubagentModelSelectionSourceConfiguredRequired - SubagentModelSelectionSourceExplicitOverride = rpc.SubagentModelSelectionSourceExplicitOverride - SubagentModelSelectionSourceRuntimePolicy = rpc.SubagentModelSelectionSourceRuntimePolicy - SubagentModelSelectionSourceSessionInheritance = rpc.SubagentModelSelectionSourceSessionInheritance - SubagentTaskModelSourceCustomAgentDefinition = rpc.SubagentTaskModelSourceCustomAgentDefinition - SubagentTaskModelSourceSubagentConfiguration = rpc.SubagentTaskModelSourceSubagentConfiguration - SubagentTaskModelSourceTaskArgument = rpc.SubagentTaskModelSourceTaskArgument - SubagentTaskModelSourceUnset = rpc.SubagentTaskModelSourceUnset - SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper - SystemMessageRoleSystem = rpc.SystemMessageRoleSystem - SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted - SystemNotificationAgentCompletedStatusFailed = rpc.SystemNotificationAgentCompletedStatusFailed - SystemNotificationTypeAgentCompleted = rpc.SystemNotificationTypeAgentCompleted - SystemNotificationTypeAgentIdle = rpc.SystemNotificationTypeAgentIdle - SystemNotificationTypeInstructionDiscovered = rpc.SystemNotificationTypeInstructionDiscovered - SystemNotificationTypeNewInboxMessage = rpc.SystemNotificationTypeNewInboxMessage - SystemNotificationTypeShellCompleted = rpc.SystemNotificationTypeShellCompleted - SystemNotificationTypeShellDetachedCompleted = rpc.SystemNotificationTypeShellDetachedCompleted - SystemNotificationTypeUnclassified = rpc.SystemNotificationTypeUnclassified - SystemNotificationTypeWorkflowCompleted = rpc.SystemNotificationTypeWorkflowCompleted - SystemNotificationWorkflowCompletedStatusCancelled = rpc.SystemNotificationWorkflowCompletedStatusCancelled - SystemNotificationWorkflowCompletedStatusCompleted = rpc.SystemNotificationWorkflowCompletedStatusCompleted - SystemNotificationWorkflowCompletedStatusError = rpc.SystemNotificationWorkflowCompletedStatusError - SystemNotificationWorkflowCompletedStatusHalted = rpc.SystemNotificationWorkflowCompletedStatusHalted - SystemNotificationWorkflowCompletedStatusPaused = rpc.SystemNotificationWorkflowCompletedStatusPaused - SystemNotificationWorkflowPauseInfoTypeCheckpoint = rpc.SystemNotificationWorkflowPauseInfoTypeCheckpoint - SystemNotificationWorkflowPauseInfoTypeUser = rpc.SystemNotificationWorkflowPauseInfoTypeUser - TaskBlockerKindPermissionRecovery = rpc.TaskBlockerKindPermissionRecovery - TaskCompletionOutcomeBlocked = rpc.TaskCompletionOutcomeBlocked - TaskCompletionOutcomeCompleted = rpc.TaskCompletionOutcomeCompleted - TaskCompletionOutcomeContinue = rpc.TaskCompletionOutcomeContinue - ToolExecutionCompleteContentResourceLinkIconThemeDark = rpc.ToolExecutionCompleteContentResourceLinkIconThemeDark - ToolExecutionCompleteContentResourceLinkIconThemeLight = rpc.ToolExecutionCompleteContentResourceLinkIconThemeLight - ToolExecutionCompleteContentTypeAudio = rpc.ToolExecutionCompleteContentTypeAudio - ToolExecutionCompleteContentTypeImage = rpc.ToolExecutionCompleteContentTypeImage - ToolExecutionCompleteContentTypeResource = rpc.ToolExecutionCompleteContentTypeResource - ToolExecutionCompleteContentTypeResourceLink = rpc.ToolExecutionCompleteContentTypeResourceLink - ToolExecutionCompleteContentTypeShellExit = rpc.ToolExecutionCompleteContentTypeShellExit - ToolExecutionCompleteContentTypeTerminal = rpc.ToolExecutionCompleteContentTypeTerminal - ToolExecutionCompleteContentTypeText = rpc.ToolExecutionCompleteContentTypeText - ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp - ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel - ToolExecutionStartToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityApp - ToolExecutionStartToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityModel - UIEphemeralQueryPhaseAborted = rpc.UIEphemeralQueryPhaseAborted - UIEphemeralQueryPhaseChunk = rpc.UIEphemeralQueryPhaseChunk - UIEphemeralQueryPhaseCompleted = rpc.UIEphemeralQueryPhaseCompleted - UIEphemeralQueryPhaseFailed = rpc.UIEphemeralQueryPhaseFailed - UIEphemeralQueryPhaseStarted = rpc.UIEphemeralQueryPhaseStarted - UserMessageAgentModeAutopilot = rpc.UserMessageAgentModeAutopilot - UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive - UserMessageAgentModePlan = rpc.UserMessageAgentModePlan - UserMessageAgentModeShell = rpc.UserMessageAgentModeShell - UserMessageDeliveryIdle = rpc.UserMessageDeliveryIdle - UserMessageDeliveryQueued = rpc.UserMessageDeliveryQueued - UserMessageDeliverySteering = rpc.UserMessageDeliverySteering - UserToolSessionApprovalKindCommands = rpc.UserToolSessionApprovalKindCommands - UserToolSessionApprovalKindCustomTool = rpc.UserToolSessionApprovalKindCustomTool - UserToolSessionApprovalKindExtensionEnvAccess = rpc.UserToolSessionApprovalKindExtensionEnvAccess - UserToolSessionApprovalKindExtensionManagement = rpc.UserToolSessionApprovalKindExtensionManagement - UserToolSessionApprovalKindExtensionPermissionAccess = rpc.UserToolSessionApprovalKindExtensionPermissionAccess - UserToolSessionApprovalKindMCP = rpc.UserToolSessionApprovalKindMCP - UserToolSessionApprovalKindMemory = rpc.UserToolSessionApprovalKindMemory - UserToolSessionApprovalKindRead = rpc.UserToolSessionApprovalKindRead - UserToolSessionApprovalKindWorkflow = rpc.UserToolSessionApprovalKindWorkflow - UserToolSessionApprovalKindWrite = rpc.UserToolSessionApprovalKindWrite - VerbosityHigh = rpc.VerbosityHigh - VerbosityLow = rpc.VerbosityLow - VerbosityMedium = rpc.VerbosityMedium - WorkflowPermissionOperationAuthor = rpc.WorkflowPermissionOperationAuthor - WorkflowPermissionOperationRun = rpc.WorkflowPermissionOperationRun - WorkflowRunSettledStatusCancelled = rpc.WorkflowRunSettledStatusCancelled - WorkflowRunSettledStatusCompleted = rpc.WorkflowRunSettledStatusCompleted - WorkflowRunSettledStatusError = rpc.WorkflowRunSettledStatusError - WorkflowRunSettledStatusHalted = rpc.WorkflowRunSettledStatusHalted - WorkflowRunSettledStatusPaused = rpc.WorkflowRunSettledStatusPaused - WorkingDirectoryContextHostTypeADO = rpc.WorkingDirectoryContextHostTypeADO - WorkingDirectoryContextHostTypeGitHub = rpc.WorkingDirectoryContextHostTypeGitHub - WorkspaceFileChangedOperationCreate = rpc.WorkspaceFileChangedOperationCreate - WorkspaceFileChangedOperationUpdate = rpc.WorkspaceFileChangedOperationUpdate + // Deprecated: SessionEventTypeToolExecutionPartialResult identifies a deprecated event. + SessionEventTypeToolExecutionPartialResult = rpc.SessionEventTypeToolExecutionPartialResult + SessionEventTypeToolExecutionProgress = rpc.SessionEventTypeToolExecutionProgress + SessionEventTypeToolExecutionStart = rpc.SessionEventTypeToolExecutionStart + SessionEventTypeToolSearchActivated = rpc.SessionEventTypeToolSearchActivated + SessionEventTypeToolShellOutput = rpc.SessionEventTypeToolShellOutput + SessionEventTypeToolUserRequested = rpc.SessionEventTypeToolUserRequested + SessionEventTypeUIEphemeralQuery = rpc.SessionEventTypeUIEphemeralQuery + SessionEventTypeUserInputCompleted = rpc.SessionEventTypeUserInputCompleted + SessionEventTypeUserInputRequested = rpc.SessionEventTypeUserInputRequested + SessionEventTypeUserMessage = rpc.SessionEventTypeUserMessage + SessionEventTypeWorkflowRunSettled = rpc.SessionEventTypeWorkflowRunSettled + SessionEventTypeWorkflowRunStarted = rpc.SessionEventTypeWorkflowRunStarted + SessionEventTypeWorkflowRunUpdated = rpc.SessionEventTypeWorkflowRunUpdated + SessionLimitsExhaustedResponseActionAdd = rpc.SessionLimitsExhaustedResponseActionAdd + SessionLimitsExhaustedResponseActionCancel = rpc.SessionLimitsExhaustedResponseActionCancel + SessionLimitsExhaustedResponseActionSet = rpc.SessionLimitsExhaustedResponseActionSet + SessionLimitsExhaustedResponseActionUnset = rpc.SessionLimitsExhaustedResponseActionUnset + SessionModeAutopilot = rpc.SessionModeAutopilot + SessionModeInteractive = rpc.SessionModeInteractive + SessionModePlan = rpc.SessionModePlan + ShutdownTypeError = rpc.ShutdownTypeError + ShutdownTypeRoutine = rpc.ShutdownTypeRoutine + SkillInvokedTriggerAgentInvoked = rpc.SkillInvokedTriggerAgentInvoked + SkillInvokedTriggerContextLoad = rpc.SkillInvokedTriggerContextLoad + SkillInvokedTriggerUserInvoked = rpc.SkillInvokedTriggerUserInvoked + SkillSourceBuiltin = rpc.SkillSourceBuiltin + SkillSourceCustom = rpc.SkillSourceCustom + SkillSourceInherited = rpc.SkillSourceInherited + SkillSourcePersonalAgents = rpc.SkillSourcePersonalAgents + SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot + SkillSourcePlugin = rpc.SkillSourcePlugin + SkillSourceProject = rpc.SkillSourceProject + SkillSourceSDK = rpc.SkillSourceSDK + SubagentModelSelectionSourceAgentDefinitionDefault = rpc.SubagentModelSelectionSourceAgentDefinitionDefault + SubagentModelSelectionSourceComplementaryDefault = rpc.SubagentModelSelectionSourceComplementaryDefault + SubagentModelSelectionSourceConfiguredPreference = rpc.SubagentModelSelectionSourceConfiguredPreference + SubagentModelSelectionSourceConfiguredRequired = rpc.SubagentModelSelectionSourceConfiguredRequired + SubagentModelSelectionSourceExplicitOverride = rpc.SubagentModelSelectionSourceExplicitOverride + SubagentModelSelectionSourceRuntimePolicy = rpc.SubagentModelSelectionSourceRuntimePolicy + SubagentModelSelectionSourceSessionInheritance = rpc.SubagentModelSelectionSourceSessionInheritance + SubagentTaskModelSourceCustomAgentDefinition = rpc.SubagentTaskModelSourceCustomAgentDefinition + SubagentTaskModelSourceSubagentConfiguration = rpc.SubagentTaskModelSourceSubagentConfiguration + SubagentTaskModelSourceTaskArgument = rpc.SubagentTaskModelSourceTaskArgument + SubagentTaskModelSourceUnset = rpc.SubagentTaskModelSourceUnset + SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper + SystemMessageRoleSystem = rpc.SystemMessageRoleSystem + SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted + SystemNotificationAgentCompletedStatusFailed = rpc.SystemNotificationAgentCompletedStatusFailed + SystemNotificationTypeAgentCompleted = rpc.SystemNotificationTypeAgentCompleted + SystemNotificationTypeAgentIdle = rpc.SystemNotificationTypeAgentIdle + SystemNotificationTypeInstructionDiscovered = rpc.SystemNotificationTypeInstructionDiscovered + SystemNotificationTypeNewInboxMessage = rpc.SystemNotificationTypeNewInboxMessage + SystemNotificationTypeShellCompleted = rpc.SystemNotificationTypeShellCompleted + SystemNotificationTypeShellDetachedCompleted = rpc.SystemNotificationTypeShellDetachedCompleted + SystemNotificationTypeUnclassified = rpc.SystemNotificationTypeUnclassified + SystemNotificationTypeWorkflowCompleted = rpc.SystemNotificationTypeWorkflowCompleted + SystemNotificationWorkflowCompletedStatusCancelled = rpc.SystemNotificationWorkflowCompletedStatusCancelled + SystemNotificationWorkflowCompletedStatusCompleted = rpc.SystemNotificationWorkflowCompletedStatusCompleted + SystemNotificationWorkflowCompletedStatusError = rpc.SystemNotificationWorkflowCompletedStatusError + SystemNotificationWorkflowCompletedStatusHalted = rpc.SystemNotificationWorkflowCompletedStatusHalted + SystemNotificationWorkflowCompletedStatusPaused = rpc.SystemNotificationWorkflowCompletedStatusPaused + SystemNotificationWorkflowPauseInfoTypeCheckpoint = rpc.SystemNotificationWorkflowPauseInfoTypeCheckpoint + SystemNotificationWorkflowPauseInfoTypeUser = rpc.SystemNotificationWorkflowPauseInfoTypeUser + TaskBlockerKindPermissionRecovery = rpc.TaskBlockerKindPermissionRecovery + TaskCompletionOutcomeBlocked = rpc.TaskCompletionOutcomeBlocked + TaskCompletionOutcomeCompleted = rpc.TaskCompletionOutcomeCompleted + TaskCompletionOutcomeContinue = rpc.TaskCompletionOutcomeContinue + ToolExecutionCompleteContentResourceLinkIconThemeDark = rpc.ToolExecutionCompleteContentResourceLinkIconThemeDark + ToolExecutionCompleteContentResourceLinkIconThemeLight = rpc.ToolExecutionCompleteContentResourceLinkIconThemeLight + ToolExecutionCompleteContentTypeAudio = rpc.ToolExecutionCompleteContentTypeAudio + ToolExecutionCompleteContentTypeImage = rpc.ToolExecutionCompleteContentTypeImage + ToolExecutionCompleteContentTypeResource = rpc.ToolExecutionCompleteContentTypeResource + ToolExecutionCompleteContentTypeResourceLink = rpc.ToolExecutionCompleteContentTypeResourceLink + ToolExecutionCompleteContentTypeShellExit = rpc.ToolExecutionCompleteContentTypeShellExit + ToolExecutionCompleteContentTypeTerminal = rpc.ToolExecutionCompleteContentTypeTerminal + ToolExecutionCompleteContentTypeText = rpc.ToolExecutionCompleteContentTypeText + ToolExecutionCompleteFileEditKindCreate = rpc.ToolExecutionCompleteFileEditKindCreate + ToolExecutionCompleteFileEditKindDelete = rpc.ToolExecutionCompleteFileEditKindDelete + ToolExecutionCompleteFileEditKindEdit = rpc.ToolExecutionCompleteFileEditKindEdit + ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp + ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel + ToolExecutionStartToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityApp + ToolExecutionStartToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionStartToolDescriptionMetaUIVisibilityModel + ToolShellOutputStreamStderr = rpc.ToolShellOutputStreamStderr + ToolShellOutputStreamStdout = rpc.ToolShellOutputStreamStdout + ToolShellOutputStreamTerminal = rpc.ToolShellOutputStreamTerminal + UIEphemeralQueryPhaseAborted = rpc.UIEphemeralQueryPhaseAborted + UIEphemeralQueryPhaseChunk = rpc.UIEphemeralQueryPhaseChunk + UIEphemeralQueryPhaseCompleted = rpc.UIEphemeralQueryPhaseCompleted + UIEphemeralQueryPhaseFailed = rpc.UIEphemeralQueryPhaseFailed + UIEphemeralQueryPhaseStarted = rpc.UIEphemeralQueryPhaseStarted + UserMessageAgentModeAutopilot = rpc.UserMessageAgentModeAutopilot + UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive + UserMessageAgentModePlan = rpc.UserMessageAgentModePlan + UserMessageAgentModeShell = rpc.UserMessageAgentModeShell + UserMessageDeliveryIdle = rpc.UserMessageDeliveryIdle + UserMessageDeliveryQueued = rpc.UserMessageDeliveryQueued + UserMessageDeliverySteering = rpc.UserMessageDeliverySteering + UserToolSessionApprovalKindCommands = rpc.UserToolSessionApprovalKindCommands + UserToolSessionApprovalKindCustomTool = rpc.UserToolSessionApprovalKindCustomTool + UserToolSessionApprovalKindExtensionEnvAccess = rpc.UserToolSessionApprovalKindExtensionEnvAccess + UserToolSessionApprovalKindExtensionManagement = rpc.UserToolSessionApprovalKindExtensionManagement + UserToolSessionApprovalKindExtensionPermissionAccess = rpc.UserToolSessionApprovalKindExtensionPermissionAccess + UserToolSessionApprovalKindMCP = rpc.UserToolSessionApprovalKindMCP + UserToolSessionApprovalKindMemory = rpc.UserToolSessionApprovalKindMemory + UserToolSessionApprovalKindRead = rpc.UserToolSessionApprovalKindRead + UserToolSessionApprovalKindWorkflow = rpc.UserToolSessionApprovalKindWorkflow + UserToolSessionApprovalKindWrite = rpc.UserToolSessionApprovalKindWrite + VerbosityHigh = rpc.VerbosityHigh + VerbosityLow = rpc.VerbosityLow + VerbosityMedium = rpc.VerbosityMedium + WorkflowPermissionOperationAuthor = rpc.WorkflowPermissionOperationAuthor + WorkflowPermissionOperationRun = rpc.WorkflowPermissionOperationRun + WorkflowRunSettledStatusCancelled = rpc.WorkflowRunSettledStatusCancelled + WorkflowRunSettledStatusCompleted = rpc.WorkflowRunSettledStatusCompleted + WorkflowRunSettledStatusError = rpc.WorkflowRunSettledStatusError + WorkflowRunSettledStatusHalted = rpc.WorkflowRunSettledStatusHalted + WorkflowRunSettledStatusPaused = rpc.WorkflowRunSettledStatusPaused + WorkingDirectoryContextHostTypeADO = rpc.WorkingDirectoryContextHostTypeADO + WorkingDirectoryContextHostTypeGitHub = rpc.WorkingDirectoryContextHostTypeGitHub + WorkspaceFileChangedOperationCreate = rpc.WorkspaceFileChangedOperationCreate + WorkspaceFileChangedOperationUpdate = rpc.WorkspaceFileChangedOperationUpdate ) diff --git a/java/AGENTS.md b/java/AGENTS.md index 684dcdb541..45b9fb9709 100644 --- a/java/AGENTS.md +++ b/java/AGENTS.md @@ -66,7 +66,7 @@ Add new Java E2E tests for Java SDK surface behavior, not shared runtime functionality; new SDK-accessible runtime E2Es belong in `../nodejs/test/e2e/`. For replay-backed Java integration tests, use the on-demand [`sdk-java-e2e-test` skill](../.github/skills/sdk-java-e2e-test/SKILL.md). -Its snapshot workflow and companion examples are not required for unrelated +Its snapshot workflow is not required for unrelated Java edits. For JDK 17 compatibility testing, run the JDK 25-built artifact on JDK 17 without recompiling it, following [Development Setup](README.md#development-setup). diff --git a/java/README.md b/java/README.md index 89ff6e5ac9..18749e0ac2 100644 --- a/java/README.md +++ b/java/README.md @@ -15,6 +15,8 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. The Java SDK tracks the official GitHub Copilot SDK family (TypeScript, Python, Go, .NET, and Rust). +The generated `SessionFsSetProviderCapabilities.binary` field describes the runtime protocol, not a Java provider API. The Java SDK cannot currently register a session filesystem provider or handle `sessionFs.readFileBytes` or `sessionFs.writeFileBytes`; do not advertise binary operations from Java. Java provider and binary support is deferred until the SDK exposes provider registration and reverse-RPC dispatch. Provider-only images currently require a provider-capable SDK. + ## Prerequisites To use the SDK, you'll need: @@ -74,7 +76,7 @@ implementation 'com.github:copilot-sdk-java:1.0.15-preview.1-SNAPSHOT' ## In-process mode (experimental) -The SDK supports running the Copilot runtime **in-process** as a native library instead of spawning a separate CLI process. This eliminates process management overhead and simplifies deployment. In-process mode is currently experimental and supported on **linux-x64** (glibc), **linux-arm64** (glibc), **linuxmusl-x64**, **win32-x64**, **win32-arm64**, **darwin-x64**, and **darwin-arm64**. +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), **linuxmusl-x64**, **linuxmusl-arm64**, **win32-x64**, **win32-arm64**, **darwin-x64**, and **darwin-arm64**. Because in-process mode is experimental, see the [Using experimental APIs](#using-experimental-apis) section for how to opt in. @@ -97,7 +99,7 @@ Add both the SDK and the platform-specific native runtime to your project: ${copilot.version} linux-x64 - + net.java.dev.jna @@ -190,6 +192,37 @@ 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. +### Subagent lifecycle hooks + +Register `setOnSubagentStart` and `setOnSubagentStop` on `SessionHooks` to inspect +the parent session and subagent metadata around each child run: + +```java +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SessionHooks; +import com.github.copilot.rpc.SubagentStartHookOutput; +import com.github.copilot.rpc.SubagentStopHookOutput; +import java.util.concurrent.CompletableFuture; + +var hooks = new SessionHooks() + .setOnSubagentStart((input, invocation) -> + CompletableFuture.completedFuture( + new SubagentStartHookOutput("Check the relevant tests first."))) + .setOnSubagentStop((input, invocation) -> + CompletableFuture.completedFuture( + new SubagentStopHookOutput(null, null, input.response()))); +var config = new SessionConfig().setHooks(hooks); +``` + +`SubagentStartHookInput` includes the parent `sessionId`, `timestamp`, `cwd`, +`transcriptPath`, `agentName`, and optional display name and description. Its +`additionalContext` output is prepended to the child's first prompt. +`SubagentStopHookInput` also includes `agentType`, optional `agentId`, +`stopReason`, and the last assistant `response`. Return a stop output with +`decision` set to `"block"` and a nonempty `reason` to request another child +turn, or set `modifiedResponse` to replace the final response. A block takes +precedence over a rewrite. + `CopilotClientOptions.setExtensionLaunchProvider(...)` configures an experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can @@ -224,10 +257,13 @@ structured form-based `ask_user` tool when an elicitation handler is also set. The default is `AskUserVariant.LEGACY`. Re-supply the option and handler through `ResumeSessionConfig` on a cold resume. -To continue a pending turn after resuming a session, pass -`new ResumeSessionConfig().setContinuePendingWork(true)` to `resumeSession`. -Set it to `false` to opt out explicitly, or leave it unset to use the runtime -default. +`ResumeSessionConfig.setContinuePendingWork(false)` interrupts work still in +flight when resuming (the default), while preserving completed tool results +already durably recorded by the runtime. +Pass `new ResumeSessionConfig().setContinuePendingWork(true)` to +`resumeSession` to keep waiting for pending tool calls and permission requests +instead. Leave the option unset to use the runtime default. Re-register any +external tools needed to handle continued work. For rotating per-session GitHub credentials, use `SessionConfig.setGitHubTokenProvider(...)` (or the equivalent @@ -250,6 +286,51 @@ provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +### Skill providers (experimental) + +`SessionConfig.setSkillProvider(...)` registers session-scoped skills that the +runtime can list and read on demand. The API is experimental; see +[Using experimental APIs](#using-experimental-apis) before compiling code that +references it. + +```java +import com.github.copilot.SkillProvider; +import com.github.copilot.SkillProviderDescriptor; + +SkillProvider provider = new SkillProvider() { + @Override + public CompletableFuture> listSkills() { + return CompletableFuture.completedFuture(List.of( + new SkillProviderDescriptor("team-plan", "Team planning guidance", true, false, null))); + } + + @Override + public CompletableFuture readSkill(String name) { + if (!name.equals("team-plan")) { + return CompletableFuture.completedFuture(null); // not found + } + return CompletableFuture.completedFuture("# Team plan\nWrite a concise monthly team plan."); + } +}; + +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setEnableSkills(true) + .setSkillProvider(provider); +``` + +Provider callbacks may be invoked concurrently, so implementations should be +thread-safe. When the runtime cancels a call, for example after its 30-second +limit or when the session disconnects, the SDK calls `cancel(true)` on the +returned future; check `isCancelled()` or attach a completion callback to stop +early. The provider is not persisted with the session; pass it again via +`ResumeSessionConfig.setSkillProvider(...)` on every resume. Resuming without a +provider unbinds any provider the session had. In +`CopilotClientMode.EMPTY`, explicitly set `setEnableSkills(true)` if the +assistant should use skills, because empty mode disables skill loading by +default. Skill providers are local-session only and are rejected for cloud +sessions. + ### Typed MCP installation and removal payloads (breaking change) Three payloads in the experimental MCP installation and removal workflow are now sealed @@ -262,6 +343,12 @@ with the other SDKs. No other generated type changes. | `McpInstallPlan.transportChoices()` | `List` | `List` (`McpPlanTransportChoicePackage` / `McpPlanTransportChoiceRemote`, by `installMethod`) | | `McpInstallationManagementResultOutcome.getOutcome()` / `setOutcome(...)` | `Object` | `McpInstallationManagementOutcome`, whose operation variant carries `McpInstallationOperationStatus` (by `phase`) | +## System Message Customization + +Use `SessionConfig.setSystemMessage(...)` with a `SystemMessageConfig` in `SystemMessageMode.CUSTOMIZE` to override individual prompt sections. Section keys are defined by `SystemMessageSections`; use `SectionOverride.setAction(...)` for static overrides or `SectionOverride.setTransform(...)` for a transform callback. + +`SystemMessageSections.LAST_INSTRUCTIONS` (`last_instructions`) includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section also removes that guidance; a transform callback receives the complete section, including the guidance, and its returned content is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `SystemMessageSections.RUNTIME_INSTRUCTIONS` is a separate section: removing it does not remove `SystemMessageSections.LAST_INSTRUCTIONS`. + ## Message source Use `MessageSource.SYSTEM` for application-generated system context and @@ -308,6 +395,17 @@ Inventory inventory = session.sendAndWait( ).get(); ``` +On resume, transcript recovery defaults to true in all modes. +Use `ResumeSessionConfig.setAllowTranscriptRecovery(false)` to reject recovery. +A repaired session exposes +`getTranscriptRecovery()` (or null), with `plannedBackupPath`, +`invalidLineNumbers` (including discarded torn-tail lines), and +`sessionStartMoved`. A rejected resume retains the existing error message; +`JsonRpcException.getCode()` and `getData()` expose the server's code and +`invalidLineNumbers` / `sessionStartMoved` data. +Disabling recovery still permits adding a missing newline after an intact final +record; it rejects torn tails. + Enable annotation processing with `CopilotResponseProcessor` (automatically discoverable alongside the SDK's existing processors), and opt in to experimental APIs as described below. The processor reuses the custom-tool `SchemaGenerator`, @@ -463,6 +561,54 @@ Chain fluent modifiers to set tool options: For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md). +### String-schema `apply_patch` overrides + +An explicit `apply_patch` override can declare `Map.of("type", "string")`. +The model sees a required `input` property, but the runtime restores the scalar +patch text before dispatch. Use `ToolInvocation.getArgumentsAs(String.class)` +to read it; `getArguments()` is for object-shaped arguments. This example +returns trimmed patch text; replace the handler body with your own patch +implementation: + +```java +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import com.github.copilot.rpc.ToolDefinition; + +ToolDefinition applyPatch = ToolDefinition.create("apply_patch", "Apply a patch", Map.of("type", "string"), + invocation -> { + String patch = invocation.getArgumentsAs(String.class); + return CompletableFuture.completedFuture(patch.trim()); + }).overridesBuiltInTool(true); +``` + +Register this definition with `SessionConfig.setTools(...)`. String-schema +`apply_patch` overrides cannot contain JSON Schema references; use an object +schema if references are needed. + +## Changing tools on a live session (experimental) + +`CopilotSession.setTools(List)` replaces the external tools +registered by this Java client without recreating the session: + +```java +session.setTools(List.of( + ToolDefinition.from("lookup_fruit", "Looks up fruit by code", + Param.of(Integer.class, "code", "Fruit code"), + code -> "dragonfruit") +)).get(); +``` + +The list is a complete replacement for this client only. Built-in tools, MCP +tools, plugin tools, and tools registered by other connections are unchanged. +Pass an empty list to remove this client's tools. Handlers switch after the +runtime accepts the replacement; rejected replacements leave the previous +handlers in place. Tool calls already running finish on the handler that started +them. + +For cross-SDK behavior and active-turn caveats, see +[changing tools](../docs/features/changing-tools.md). + ## Auto routing tiers Use `CapiSessionOptions.setAutoTier(...)` to select `AutoTier.EFFICIENCY`, @@ -617,6 +763,11 @@ observers share the failed future. Some SDK APIs are marked as experimental with `@CopilotExperimental`. These APIs may change or be removed in future versions without notice. +An otherwise stable session event can contain experimental properties. Generated +event payload records preserve each property's schema stability marker on its +record component and generated accessor; the enclosing event and its other +properties remain stable. + By default, referencing an experimental API from your code causes a **compile-time error**: ``` @@ -769,7 +920,7 @@ CI enforces both checks. Spotless runs explicitly in CI; `mvn verify` alone does Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js in addition to JDK 25 and Maven. In a standalone SDK checkout, `copilot-native/scripts/fetch-native.mjs` retrieves the pinned runtime package from the corresponding GitHub release. When the SDK is nested in `copilot-agent-runtime`, it instead stages the same-checkout artifacts from `dist-cli`; run `pnpm run build:cli` from the runtime repository first. -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 a Linux musl x64 host, Maven activates `native-linuxmusl-x64` when `copilot.native.libc=musl` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, stages the platform package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. +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 a Linux musl host, Maven activates `native-linuxmusl-x64` or `native-linuxmusl-arm64` for the matching architecture when `copilot.native.libc=musl` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, stages the platform package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Before opting in, validate that Node.js reports glibc for the build host: @@ -804,14 +955,14 @@ node copilot-native/scripts/validate-native-host.mjs linux-arm64 mvn -Pinprocess clean verify -Dcopilot.native.libc=glibc ``` -The same command validates in-process mode on Linux musl x64: +The same command validates in-process mode on Linux musl x64 or ARM64: ```bash -node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 +node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 # Use linuxmusl-arm64 on ARM64 mvn -Pinprocess clean verify -Dcopilot.native.libc=musl ``` -On Linux musl ARM64 and other unsupported hosts, do not set `copilot.native.libc`. 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. +On unsupported hosts, do not set `copilot.native.libc`. 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: @@ -833,17 +984,18 @@ Each classifier JAR includes `runtime.node`, `platform.properties`, and `copilot ### Versioning and releases -The Java SDK uses [Maven CI-friendly versions](https://maven.apache.org/maven-ci-friendly.html). Every module declares `${revision}`, and the single source of truth is the `` property in `java/pom.xml`. The committed value stays a `-SNAPSHOT` (for example `1.0.14-SNAPSHOT`) and is only used for local development and the daily snapshot publish. +The Java SDK uses [Maven CI-friendly versions](https://maven.apache.org/maven-ci-friendly.html). Every module declares `${revision}`, and the single source of truth is the `` property in `java/pom.xml`. The committed value stays a `-SNAPSHOT` for local development. Published artifacts contain a concrete version rather than the unresolved `${revision}` property. -Releasing is intentionally a **read-only** operation that never mutates the repository: +Stable, prerelease, and public unstable releases include all six SDKs. SDK and runtime versions are numbered independently; each Java release contains the matching runtime artifacts in its native classifier JARs. Public unstable versions use `X.Y.Z-unstable..g`. Maven `-SNAPSHOT` builds are a separate development channel. -- The release version is computed by the shared release pipeline (`.github/workflows/publish.yml`) — the same version used by every other language SDK — and injected at build time with `-Drevision=X.Y.Z`. The POM is **not** edited or committed. -- `.github/workflows/java-publish-maven.yml` builds every native classifier and the primary artifact from a single immutable source commit and publishes to Maven Central. It creates no commits, no branch-protection bypass, and requires no elevated repository token. -- The `java/vX.Y.Z` traceability tag and the cross-language `vX.Y.Z` GitHub Release are created by `publish.yml` **after** publication succeeds, pointing at the original release commit. +Release artifacts and source references are public: -For an independent Java publication retry, dispatch `java-publish-maven.yml` from `main` with the original `releaseVersion` and full `sourceSha`. The source must be a commit already in `main`'s history. Unmerged commits, branch names, and tag names are rejected before builds run. +- Java packages are available from Maven Central. +- The `java/v` and `v` tags identify the corresponding public SDK source snapshot in [`github/copilot-sdk`](https://github.com/github/copilot-sdk). +- Stable/prerelease versions also have a combined `v` [GitHub release](https://github.com/github/copilot-sdk/releases) and versioned Java documentation. Prerelease documentation does not replace the latest documentation. +- Unstable releases create source tags without advancing SDK `main`, creating an SDK GitHub release announcement, or deploying Java documentation. Their runtime assets are available in the separate `runtime-` release. -Because there is no `maven-release-plugin` and no `release:prepare` ceremony, the POM deliberately does not track the "next" release version. To validate a build with an explicit version locally, without publishing: +To validate a build with an explicit version locally, without changing the checked-in POM or publishing: ```bash # Build and verify with an explicit version, without touching the POM @@ -855,7 +1007,7 @@ cat sdk/.flattened-pom.xml copilot-native/.flattened-pom.xml These commands do not upload artifacts. Do not use `deploy` for local validation: the Central publishing plugin is configured with `autoPublish=true`. -`flatten-maven-plugin` (ossrh mode) resolves `${revision}` into the installed and published POMs, so downstream consumers never see the unresolved property. Documentation version references are updated through a normal reviewed pull request (see `scripts/update-documentation-versions.sh`), not as a side effect of publishing. +`flatten-maven-plugin` (ossrh mode) resolves `${revision}` into the installed and published POMs. Documentation version references are updated through a normal reviewed pull request (see `scripts/update-documentation-versions.sh`), not as a side effect of publishing. ## License diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index 2c750e4e47..41af2dd6cb 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -467,6 +467,64 @@ + + native-linuxmusl-arm64 + + + Linux + aarch64 + + + copilot.native.libc + musl + + + + linuxmusl-arm64 + + + + + 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 @@ -809,6 +867,68 @@ + + + attach-external-linuxmusl-arm64-classifier + + + copilot.native.external.linuxmusl.arm64.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-linuxmusl-arm64-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + linuxmusl-arm64 + ${copilot.native.external.linuxmusl.arm64.classifier.path} + ${project.build.finalName}-linuxmusl-arm64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-linuxmusl-arm64-classifier + package + + attach-artifact + + + + + ${copilot.native.external.linuxmusl.arm64.classifier.path} + jar + linuxmusl-arm64 + + + + + + + + + + jna + ${project.build.directory}/consumer-dependencies + + + + + # GitHub Copilot CLI SDK for Rust A Rust SDK for programmatic access to the GitHub Copilot CLI. @@ -101,6 +103,13 @@ let pong = client.ping("hello").await?; client.stop().await?; ``` +`ResumeSessionConfig::with_allow_transcript_recovery(false)` rejects a resume that would +discard or reorder transcript records, but permits adding a missing newline after +an intact final record. Recovery defaults to `true` in all modes. +When allowed and performed, `session.transcript_recovery()` returns +the planned backup path, invalid line numbers, and whether `session.start` was moved; +the backup is written on the next append rather than during resume. + After `Client::start` succeeds, inspect its startup cost without parsing logs: ```rust,ignore @@ -425,6 +434,59 @@ session session.disconnect().await?; ``` +#### Skill providers (experimental) + +Hosts can supply a session-scoped skill catalog and lazy markdown reader with +`SkillProvider`. The provider is runtime-only: the SDK sends only +`hasSkillProvider: true` on `session.create` / `session.resume`, and routes +`skillProvider.list` and `skillProvider.read` callbacks back to the trait. + +```rust,no_run +use std::sync::Arc; +use async_trait::async_trait; +use github_copilot_sdk::skill_provider::{SkillProvider, SkillProviderDescriptor}; +use github_copilot_sdk::{Client, ClientOptions, Error, SessionConfig}; + +struct AppSkills; + +#[async_trait] +impl SkillProvider for AppSkills { + async fn list_skills( + &self, + ) -> Result, Error> { + Ok(vec![SkillProviderDescriptor { + name: "review".to_string(), + description: "Review the current change".to_string(), + ..Default::default() + }]) + } + + async fn read_skill(&self, name: &str) -> Result, Error> { + Ok((name == "review").then(|| "# Review\nInspect the diff carefully.".to_string())) + } +} + +# async fn example() -> Result<(), Error> { +let client = Client::start(ClientOptions::default()).await?; +let session = client + .create_session(SessionConfig::default().with_skill_provider(Arc::new(AppSkills))) + .await?; +# session.disconnect().await?; +# client.stop().await.ok(); +# Ok(()) +# } +``` + +In `ClientMode::Empty`, built-in skill loading defaults to disabled; set +`enable_skills` to `Some(true)` when the session should use provider-backed +skills in that mode. Providers are not persisted, so re-supply one with +`ResumeSessionConfig::with_skill_provider` on every resume. Cloud sessions do +not support skill providers. Each callback is dispatched on its own spawned +task, so provider implementations must be safe for concurrent calls. When the +runtime cancels a call, for example after its 30-second limit or when the +session disconnects, the SDK drops the provider future; await cancel-safe work +so that dropping it stops the lookup. + #### Typed RPC namespace High-level helpers are convenience wrappers over a fully-typed @@ -774,12 +836,14 @@ let session = client .await?; ``` -**Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. Return `HookOutput::None` for events you don't handle. +**Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`, `AgentStop`, `SubagentStart`, and `SubagentStop`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. `on_subagent_start` can inject `additional_context` into the child agent's initial prompt; `on_subagent_stop` can return a `decision` of `"block"` with a `reason` to continue the child or a `modified_response` to replace its final answer. Return `HookOutput::None` for events you don't handle. ### System Message Transforms Transforms customize system message sections during session creation. The SDK injects `action: "transform"` entries for each section ID your transform handles. +`last_instructions` includes configured subagent-model guidance when the `task` tool is available. Removing or replacing this section in customize mode also removes that guidance; a `SystemMessageTransform` handling this section receives its complete content, including the guidance, and any replacement it returns is authoritative. Append, prepend, and preserve retain their usual section semantics. These overrides change prompt prose only, not configured subagent models, tool availability, or runtime dispatch policy. `runtime_instructions` is a separate section: removing it does not remove `last_instructions`. + ```rust,ignore use github_copilot_sdk::transforms::*; use async_trait::async_trait; @@ -886,6 +950,44 @@ The closure receives the full [`ToolInvocation`](crate::types::ToolInvocation) a Reach for the `ToolHandler` trait directly when you need shared state across multiple methods or want a named type that shows up by name in stack traces. +#### Replacing tools during a session + +`Session::set_tools` (experimental) replaces the complete set of tools this client supplies to a live session, together with the handlers that serve them. It takes the same `Tool` values as `with_tools`, and an empty collection removes all of this client's tools. Built-in, MCP, plugin, and extension tools, and tools that other connected clients supply, are unaffected. + +```rust,ignore +session.set_tools(vec![search_tool, filter_tool]).await?; +``` + +The agent sees the new tools from its next model request. The new handlers take effect as soon as the runtime accepts the replacement, and calls already running finish on the handlers that started them. If the runtime rejects the replacement, nothing changes. A model request already in flight was made with the previous tools, so the agent can still call a tool you removed; this session won't answer that call, so replace tools while the session is idle if a running turn might still call a tool you remove. See [Changing tools during a session](../docs/features/changing-tools.md) for the behavior shared by all SDKs. + +#### String-schema `apply_patch` overrides + +With the `derive` feature enabled, an explicit `apply_patch` override can use +`define_tool::` to declare a root string schema. The model sees a +required `input` property, but the runtime restores the scalar patch text before +dispatch. The typed handler receives a `String`, not an `{"input": ...}` object. +This example returns trimmed patch text; replace the handler body with your own +patch implementation: + +```rust,ignore +use github_copilot_sdk::tool::define_tool; +use github_copilot_sdk::ToolResult; + +let apply_patch = define_tool::( + "apply_patch", + "Apply a patch", + |_invocation, patch| async move { + Ok(ToolResult::Text(patch.trim().to_owned())) + }, +) +.with_overrides_built_in_tool(true); + +let config = config.with_tools(vec![apply_patch]); +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + ### Permission Policies Set a permission policy directly on `SessionConfig` with the chainable builders. They install a synthesized `PermissionHandler` so only permission requests are intercepted; every other event flows through unchanged. @@ -1320,6 +1422,16 @@ let session = client See [`examples/session_fs.rs`](examples/session_fs.rs) for a complete in-memory provider implementation. +To let the `view` tool read images stored only in that provider, enable +`SessionFsCapabilities::new().with_binary(true)` with +`SessionFsConfig::with_capabilities`, implement `SessionFsBinaryProvider` +(`read_file_bytes` and `write_file_bytes`), and return it from +`SessionFsProvider::binary`. A registered provider +without binary support does not fall back to a local file with the same +path. + +Binary reads and writes are limited to 50,330,880 raw bytes (approximately 48 MiB); +larger results return a filesystem error before encoding or decoding. - **Canvas action dispatch is a single trait method, not per-action closures.** The Node SDK binds an optional `handler` closure on each entry of a canvas's @@ -1451,7 +1563,21 @@ explicit program path is supplied. (present in published crate tarballs and vendored slots). - Otherwise, `../nodejs/package.json` (contributor build inside the github/copilot-sdk repo). - When SDK-managed acquisition is enabled, the resolved version is baked into the crate via `cargo:rustc-env=COPILOT_SDK_CLI_VERSION`. The runtime resolver consumes it to recompute the on-disk path by convention, so no absolute paths leak into the rlib. + When SDK-managed acquisition is enabled, the resolved version is baked into the crate via `cargo:rustc-env=COPILOT_SDK_CLI_VERSION`. A release-scoped `COPILOT_SDK_CLI_CACHE_ID` lets the runtime resolver recompute the on-disk path without leaking absolute build-machine paths into the rlib. + + Stable/prerelease snapshots, including existing published crates, acquire + assets from `github/copilot-cli` at `v`. Public unstable + snapshots additionally pin + `release-url=https://github.com/github/copilot-sdk/releases/download/runtime-`. + Both snapshots must agree on the version and release URL. Executables, + runtime packages, and checksum lookups all use that exact public release; + consumers need no credentials or unstable-specific environment settings. + + Source checkouts without snapshots infer the SDK-hosted release for canonical + unstable pins in `nodejs/package.json`, including both + `X.Y.Z-unstable.r.g` and `X.Y.Z-N.unstable.r.g`. + Other source pins and legacy snapshots without `release-url` continue to use + the CLI release location. 2. **Build time:** `build.rs` downloads the platform-specific full CLI archive and runtime package, then verifies both SHA-256 hashes against the release's @@ -1484,6 +1610,10 @@ explicit program path is supplied. application. Old version directories accumulate in siblings; clean them up at your leisure. + Public unstable cache directories use + `copilot-sdk-runtime-` instead of `` to keep release + destinations separate. Legacy cache paths remain unchanged. + ### Overriding the extraction location [`ClientOptions::with_bundled_cli_extract_dir`] redirects embed-mode extraction to a custom directory (CI runners with ephemeral homes, sandboxes that disallow cache paths, etc.): @@ -1575,9 +1705,61 @@ In embed mode `build.rs` downloads both verified archives on every clean build by default. Set `BUNDLED_CLI_CACHE_DIR=` to cache them between builds (CI keys this on `-` for near-zero-cost rebuilds on cache hits). For Copilot CLI 1.0.83-5, the two upstream archives total roughly 132-157 MB per -platform before the runtime package is filtered. With `runtime` enabled and both -`bundled-cli` and `local-runtime` disabled, -there is no separate archive cache: the extracted runtime bundle is the cache. +platform before the runtime package is filtered. With `runtime` enabled and +both `bundled-cli` and `local-runtime` disabled, +the extracted runtime bundle is the primary cache; a configured download +cache can also supply its initial extraction. + +### Preparing release snapshots before publication + +The two scripts in `scripts/` retain their no-option behavior: read +`../nodejs/package.json` and fetch the pinned CLI release's `SHA256SUMS.txt`. +Release packaging can instead supply local checksums and the final public +location, without waiting for that release to exist: + +```bash +bash scripts/snapshot-bundled-cli-version.sh \ + --version "$RUNTIME_VERSION" --release-url "$RELEASE_URL" \ + --checksums "$LOCAL_SHA256SUMS" +bash scripts/snapshot-bundled-in-process-version.sh \ + --version "$RUNTIME_VERSION" --release-url "$RELEASE_URL" \ + --checksums "$LOCAL_SHA256SUMS" +``` + +`RELEASE_URL` is the exact base URL described above, without a trailing slash. +`LOCAL_SHA256SUMS` names the staged checksum file covering all eight executable +archives and all eight `github-copilot--.tgz` payloads. +The existing `cli-version.txt` and `cli-version-in-process.txt` package entries +carry the version, hashes, and optional `release-url`; no separate manifest or +consumer configuration is required. + +For normal promotions from an older compatible source, invoke the reviewed +producer scripts with `--output` pointing to each snapshot in the selected +Rust source staging directory. Pass the selected runtime version explicitly. +This preserves the older product's build code and does not require its source +to contain these producer scripts. Public unstable releases still require +selected-source support for the SDK-hosted acquisition location. + +For offline build/package verification, seed `BUNDLED_CLI_CACHE_DIR` with the +host's executable and payload archives under these filenames: + +* CLI release: `v-` +* SDK-hosted unstable release: `copilot-sdk-runtime--` + +Archive bytes must match the snapshot hashes; cache hits are verified and +corrupt entries are evicted. The executable is `copilot-.tar.gz` (or +`.zip` on Windows); the payload is +`github-copilot--.tgz`. A non-bundled build needs only +the payload archive and can also use this seeded cache for initial extraction. +Keep `COPILOT_SKIP_CLI_DOWNLOAD` unset during acquisition verification. + +The focused acquisition checks use tiny local archive fixtures and never +download a runtime: + +```bash +node --test scripts/snapshot-version.test.mjs +cargo test --no-default-features --features local-runtime --test build_acquisition +``` ### Platforms diff --git a/rust/RELEASING.md b/rust/RELEASING.md index 06e362f54e..084cd4f887 100644 --- a/rust/RELEASING.md +++ b/rust/RELEASING.md @@ -1,92 +1,58 @@ -# Releasing `github-copilot-sdk` + -The Rust crate ships through the unified `publish.yml` workflow -alongside the other SDKs. There is no Rust-specific release workflow. +# Rust SDK releases -## TL;DR +The [`github-copilot-sdk` crate](https://crates.io/crates/github-copilot-sdk) +is published alongside the Node.js, Python, Go, .NET, and Java SDKs. SDK and +runtime versions are numbered independently; each published crate pins the +runtime version it uses. -1. Land your changes on `main`. -2. Trigger the **Publish SDK packages** workflow - (`.github/workflows/publish.yml`) via `workflow_dispatch`. -3. Pick `dist-tag`: - - `latest` — stable release (e.g. `1.0.0`). - - `prerelease` — beta release (e.g. `1.0.0-beta.4`). Lands on - crates.io as a prerelease; users must opt in with an explicit - prerelease version requirement to install it. - - `unstable` — skipped for Rust (Cargo doesn't have a clean - equivalent of npm's `unstable` dist-tag). -4. For `latest` and `prerelease`, the workflow publishes all SDKs at - the shared computed version, tags `rust/vX.Y.Z` for source - traceability, and creates one combined `vX.Y.Z` GitHub Release. - The `unstable` channel publishes only the Node.js SDK and does not - create a GitHub Release. +## Release channels -## Version, tag, and release notes +| Channel | Rust publication | +| --- | --- | +| Stable | Stable crate, released with all six SDKs and the CLI | +| Prerelease | Prerelease crate, released with all six SDKs and the CLI | +| Unstable | Development crate, released with all six SDKs and public runtime acquisition assets | -- **Crate version:** the in-tree `rust/Cargo.toml` carries `0.0.0-dev` - as a placeholder. CI overrides it at publish time with the version - computed by `publish.yml` (or an explicit `version` workflow input). -- **Tag:** `rust/vX.Y.Z` (matches the `go/vX.Y.Z` style used elsewhere - in this repo). The tag identifies the source used for that crate - version. -- **Release notes:** generated for the combined `vX.Y.Z` GitHub - Release. Write descriptive PR titles for changes that touch the Rust - surface so they are represented accurately in the shared notes. +Find available crate versions on +[crates.io](https://crates.io/crates/github-copilot-sdk/versions). -## Cargo prerelease semantics +## Runtime pins and release outputs -`cargo add github-copilot-sdk` and `version = "1"` requirements skip -prereleases by default. Users who want to opt in to a beta must -write an explicit prerelease requirement: +Published crates include `cli-version.txt` and `cli-version-in-process.txt` +with the exact runtime version, release location, and archive hashes. -```toml -github-copilot-sdk = "1.0.0-beta.4" -``` +Stable/prerelease acquisition uses +[`github/copilot-cli` releases](https://github.com/github/copilot-cli/releases). +Unstable acquisition uses the exact `runtime-` release in +[`github/copilot-sdk`](https://github.com/github/copilot-sdk/releases). +Default consumer builds acquire both +the runtime platform package and standalone CLI archive without private-feed +credentials. -This matches Cargo's standard semver behavior and means a -prerelease-channel publish won't surprise stable users. +The `rust/v` and `v` tags identify the corresponding +public SDK source snapshot for stable, prerelease, and unstable versions. +Stable/prerelease versions also have a combined `v` GitHub release. +Unstable source tags do not advance SDK `main` or create an SDK GitHub release +announcement. Their runtime assets remain available under the separate +`runtime-` release. -## Yanking a release +## Cargo prerelease semantics -If a published version contains a critical bug, yank it from -crates.io to prevent new installs: +Cargo does not have npm distribution tags. Consumers opt into a prerelease +or unstable crate by explicitly requesting its version, for example: -```sh -cargo yank --version X.Y.Z github-copilot-sdk +```toml +github-copilot-sdk = "=1.0.0-unstable.123456.gabcdef0" ``` -Yanking does *not* delete the version — existing `Cargo.lock` files -keep working — but it stops new resolutions from picking it. Follow -up with a patch release that fixes the bug, and update the combined -GitHub Release notes to explain why. - -Reverse with `cargo yank --undo --version X.Y.Z github-copilot-sdk` -if the yank was a mistake. - -## Manual publish (emergency only) +This is an illustrative version; select an available version from crates.io. +Stable requirements do not automatically select prereleases. -If GitHub Actions is unavailable, a maintainer with crates.io -credentials can publish locally: - -```sh -cd rust - -# Set the real version (replace X.Y.Z). -perl -i -pe 's/^version = ".*"$/version = "X.Y.Z"/' Cargo.toml - -# Verify package contents. -cargo publish --dry-run - -# Publish for real. -cargo publish - -# Tag and push. -git tag rust/vX.Y.Z -git push origin rust/vX.Y.Z - -# Restore the placeholder. -perl -i -pe 's/^version = ".*"$/version = "0.0.0-dev"/' Cargo.toml -``` +## Yanked versions -Manual publishes skip the combined GitHub Release. Create or update the -matching `vX.Y.Z` release after pushing the tag. +A crate version with a critical defect can be yanked. Yanking does not delete +the version or invalidate existing lockfiles, but Cargo excludes it from new +dependency resolutions. Consult the release notes and update affected +applications to a fixed version. diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs index a4bafc35af..cfae1539ca 100644 --- a/rust/build/in_process.rs +++ b/rust/build/in_process.rs @@ -1,4 +1,6 @@ -use std::io::{Read, Write}; +// Copyright (c) Microsoft Corporation. All rights reserved. + +use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -87,36 +89,35 @@ pub(crate) fn main() { // consumer; generated by the publish workflow from SHA256SUMS.txt). // 2. Sibling `../nodejs/package.json` plus the release SHA256SUMS.txt // (contributor build inside the github/copilot-sdk repo). - let (version, local_expected_hash) = resolve_version_and_optional_hash(platform.package_name); - - // Bake the version into the crate regardless of mode. This is the - // single source of truth for "what CLI version did build.rs target", - // consumed by both the embed-mode path computation in embeddedcli.rs - // and the runtime path computation in resolve.rs (when `bundled-cli` - // is off). It's a small, machine-independent datum: no absolute - // paths, no username/home leakage, so sccache / cross-machine - // `target/` reuse stays cache-coherent. + let (release, local_expected_hash) = + resolve_version_and_optional_hash(&manifest_dir, platform.package_name); + let version = &release.version; + + // Keep diagnostics on the actual version, and cache paths on a + // release-scoped identity. Neither exposes build-machine paths. println!("cargo:rustc-env=COPILOT_SDK_CLI_VERSION={version}"); + let cache_identity = release.cache_identity(); + println!("cargo:rustc-env=COPILOT_SDK_CLI_CACHE_ID={cache_identity}"); let asset_platform = platform .package_name .strip_prefix("copilot-") .expect("platform package names start with copilot-"); let archive_name = format!("github-copilot-{version}-{asset_platform}.tgz"); - let download_url = format!( - "https://github.com/github/copilot-cli/releases/download/v{version}/{archive_name}" - ); + let download_url = release.asset_url(&archive_name); let cache_dir = std::env::var("BUNDLED_CLI_CACHE_DIR") .ok() .map(std::path::PathBuf::from); - let cache_key = format!("v{version}-{archive_name}"); + let cache_key = release.cache_key(&archive_name); let include_runtime = std::env::var_os("CARGO_FEATURE_IN_PROCESS").is_some(); if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() { + let cli_asset_name = platform.cli_asset_name(); + let cli_expected_hash = resolve_cli_hash(&release, &cli_asset_name); let runtime_expected_hash = local_expected_hash .clone() - .unwrap_or_else(|| fetch_in_process_release_hash(&version, platform.package_name)); + .unwrap_or_else(|| fetch_release_hash(&release, &archive_name)); let runtime_package = cached_download( &download_url, &cache_key, @@ -125,13 +126,9 @@ pub(crate) fn main() { ); verify_runtime_package(&runtime_package, platform, &archive_name); - let cli_asset_name = platform.cli_asset_name(); - let cli_expected_hash = resolve_cli_hash(&version, &cli_asset_name); let cli_archive = cached_download( - &format!( - "https://github.com/github/copilot-cli/releases/download/v{version}/{cli_asset_name}" - ), - &format!("v{version}-{cli_asset_name}"), + &release.asset_url(&cli_asset_name), + &release.cache_key(&cli_asset_name), &cli_expected_hash, &cache_dir, ); @@ -151,10 +148,10 @@ pub(crate) fn main() { // Skip the upstream download entirely when both files already exist. // // Runtime resolution (see `src/resolve.rs::extracted_program`) - // recomputes this same path from `COPILOT_SDK_CLI_VERSION` + the + // recomputes this same path from `COPILOT_SDK_CLI_CACHE_ID` + the // OS-derived binary name + optional `COPILOT_CLI_EXTRACT_DIR`, // so we don't bake an absolute path into the crate. - let install_dir = cache_paths::extracted_runtime_install_dir(&version); + let install_dir = cache_paths::extracted_runtime_install_dir(&cache_identity); let required_paths = [ install_dir.join(platform.runtime_wrapper_name()), install_dir.join("runtime.node"), @@ -177,11 +174,11 @@ pub(crate) fn main() { } None => marker .as_deref() - .is_some_and(|contents| marker_matches_version(contents, &version)), + .is_some_and(|contents| marker_matches_version(contents, version)), }; if !cache_is_current { - let expected_hash = local_expected_hash - .unwrap_or_else(|| fetch_in_process_release_hash(&version, platform.package_name)); + let expected_hash = + local_expected_hash.unwrap_or_else(|| fetch_release_hash(&release, &archive_name)); let expected_marker = format!("{version}\n{expected_hash}\n"); if install_dir.exists() { std::fs::remove_dir_all(&install_dir).unwrap_or_else(|e| { @@ -323,6 +320,7 @@ fn hostless_runtime_path(source: &str, platform: Platform) -> Option { "app.js", "assets", "changelog.json", + "cli-main.js", "foundry-local-sdk", "index.js", "LICENSE.md", @@ -378,28 +376,28 @@ fn append_archive_file( /// Resolve the CLI version and any locally snapshotted release hash for the /// current target's platform package. Contributor builds defer fetching the /// checksum until a download is actually required. -fn resolve_version_and_optional_hash(package_name: &str) -> (String, Option) { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); - +fn resolve_version_and_optional_hash( + manifest_dir: &Path, + package_name: &str, +) -> (Release, Option) { // 1. Snapshot file at the crate root (published-crate consumer, // vendored-slot consumer). Combined version + per-asset hashes. - let snapshot = Path::new(&manifest_dir).join("cli-version-in-process.txt"); + let snapshot = manifest_dir.join("cli-version-in-process.txt"); if snapshot.is_file() { let contents = std::fs::read_to_string(&snapshot) .unwrap_or_else(|e| panic!("failed to read {}: {e}", snapshot.display())); - let (version, hash) = parse_snapshot(&contents, package_name) + let (release, hash) = parse_snapshot(&contents, package_name) .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); - return (version, Some(hash)); + return (release, Some(hash)); } // 2. Package version plus release checksums (contributor build). - let package_json = Path::new(&manifest_dir) - .join("..") - .join("nodejs") - .join("package.json"); + let package_json = manifest_dir.join("..").join("nodejs").join("package.json"); if package_json.is_file() { let version = read_version_from_package_json(&package_json); - return (version, None); + let release = Release::from_source_version(version) + .unwrap_or_else(|e| panic!("invalid {}: {e}", package_json.display())); + return (release, None); } panic!( @@ -414,31 +412,23 @@ fn resolve_version_and_optional_hash(package_name: &str) -> (String, Option String { - let platform = package_name - .strip_prefix("copilot-") - .expect("platform package names start with copilot-"); - let asset_name = format!("github-copilot-{version}-{platform}.tgz"); - fetch_release_hash(version, &asset_name) -} - -fn resolve_cli_hash(version: &str, asset_name: &str) -> String { +fn resolve_cli_hash(release: &Release, asset_name: &str) -> String { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); let snapshot = Path::new(&manifest_dir).join("cli-version.txt"); if snapshot.is_file() { let contents = std::fs::read_to_string(&snapshot) .unwrap_or_else(|e| panic!("failed to read {}: {e}", snapshot.display())); - let (snapshot_version, hash) = parse_snapshot(&contents, asset_name) + let (snapshot_release, hash) = parse_snapshot(&contents, asset_name) .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); assert_eq!( - snapshot_version, - version, - "{} and the selected runtime version source must pin the same version", + snapshot_release, + *release, + "{} and the selected runtime version source must pin the same version and release URL", snapshot.display() ); return hash; } - fetch_release_hash(version, asset_name) + fetch_release_hash(release, asset_name) } fn marker_matches_version(contents: &str, version: &str) -> bool { @@ -450,33 +440,156 @@ fn marker_matches_version(contents: &str, version: &str) -> bool { && lines.next().is_none() } -/// Parse the `cli-version-in-process.txt` snapshot file. Format is one `key=value` per -/// line. The first non-comment line is `version=X.Y.Z`; subsequent lines map -/// platform package name to SHA-256. Blank lines and lines starting with `#` -/// are skipped. -fn parse_snapshot(contents: &str, package_name: &str) -> Result<(String, String), String> { - let mut version: Option = None; - let mut hash: Option = None; +#[derive(Debug, PartialEq, Eq)] +struct Release { + version: String, + sdk_release: bool, +} + +impl Release { + /// Infer the host only for source checkouts; URL-less snapshots retain their legacy host. + fn from_source_version(version: String) -> io::Result { + let mut release = Self::new(version, None)?; + let Some((core, suffix)) = release.version.split_once('-') else { + return Ok(release); + }; + let numeric = |part: &str| { + !part.is_empty() + && (part == "0" || !part.starts_with('0')) + && part.bytes().all(|byte| byte.is_ascii_digit()) + }; + let mut components = core.split('.'); + let canonical_core = + (0..3).all(|_| components.next().is_some_and(numeric)) && components.next().is_none(); + let identity = suffix.strip_prefix("unstable.r").or_else(|| { + let (prerelease, identity) = suffix.split_once(".unstable.r")?; + numeric(prerelease).then_some(identity) + }); + release.sdk_release = canonical_core + && identity.is_some_and(|identity| { + let Some((run, sha)) = identity.split_once(".g") else { + return false; + }; + numeric(run) + && run != "0" + && sha.len() == 7 + && sha + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + }); + Ok(release) + } + + fn new(version: String, release_url: Option<&str>) -> io::Result { + if !version.starts_with(|c: char| c.is_ascii_digit()) + || !version + .bytes() + .all(|c| c.is_ascii_alphanumeric() || b".+-".contains(&c)) + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid release version", + )); + } + let cli_url = format!("https://github.com/github/copilot-cli/releases/download/v{version}"); + let sdk_url = + format!("https://github.com/github/copilot-sdk/releases/download/runtime-{version}"); + let sdk_release = match release_url { + None => false, + Some(url) if url == cli_url => false, + Some(url) if url == sdk_url => true, + Some(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "release-url must be the exact public copilot-cli/v or copilot-sdk/runtime- release URL", + )); + } + }; + Ok(Self { + version, + sdk_release, + }) + } + + fn asset_url(&self, asset_name: &str) -> String { + if self.sdk_release { + format!( + "https://github.com/github/copilot-sdk/releases/download/runtime-{}/{asset_name}", + self.version + ) + } else { + format!( + "https://github.com/github/copilot-cli/releases/download/v{}/{asset_name}", + self.version + ) + } + } + + fn cache_identity(&self) -> String { + if self.sdk_release { + format!("copilot-sdk-runtime-{}", self.version) + } else { + self.version.clone() + } + } + + fn cache_key(&self, asset_name: &str) -> String { + if self.sdk_release { + format!("{}-{asset_name}", self.cache_identity()) + } else { + format!("v{}-{asset_name}", self.version) + } + } +} + +/// Both publish snapshots use version/hash entries plus an optional exact release URL. +/// Snapshots without a URL retain the original public CLI acquisition contract. +fn parse_snapshot(contents: &str, asset_name: &str) -> io::Result<(Release, String)> { + let mut version = None; + let mut release_url = None; + let mut hash = None; for (line_no, raw) in contents.lines().enumerate() { let line = raw.trim(); if line.is_empty() || line.starts_with('#') { continue; } let Some((key, value)) = line.split_once('=') else { - return Err(format!( - "line {}: expected `key=value`, got `{raw}`", - line_no + 1 + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("line {}: expected `key=value`, got `{raw}`", line_no + 1), )); }; - match key.trim() { - "version" => version = Some(value.trim().to_string()), - k if k == package_name => hash = Some(value.trim().to_string()), - _ => {} + let slot = match key.trim() { + "version" => &mut version, + "release-url" => &mut release_url, + k if k == asset_name => &mut hash, + _ => continue, + }; + if slot.replace(value.trim()).is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("duplicate `{key}`"), + )); } } - let version = version.ok_or("missing `version=` line")?; - let hash = hash.ok_or_else(|| format!("missing hash for package `{package_name}`"))?; - Ok((version, hash)) + let version = version + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing `version=` line"))?; + let hash = hash.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("missing hash for asset `{asset_name}`"), + ) + })?; + if hash.len() != 64 || !hash.bytes().all(|c| c.is_ascii_hexdigit()) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid SHA-256 for `{asset_name}`"), + )); + } + Ok(( + Release::new(version.to_string(), release_url)?, + hash.to_ascii_lowercase(), + )) } fn read_version_from_package_json(path: &Path) -> String { @@ -490,22 +603,35 @@ fn read_version_from_package_json(path: &Path) -> String { .to_string() } -fn fetch_release_hash(version: &str, asset_name: &str) -> String { - let url = format!( - "https://github.com/github/copilot-cli/releases/download/v{version}/SHA256SUMS.txt" - ); +fn fetch_release_hash(release: &Release, asset_name: &str) -> String { + let url = release.asset_url("SHA256SUMS.txt"); let checksums = download_with_retry(&url); let checksums = std::str::from_utf8(&checksums).expect("SHA256SUMS.txt is not valid UTF-8"); find_sha256_for_asset(checksums, asset_name) } fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { - sums.lines() - .find_map(|line| { - let (hash, name) = line.split_once(char::is_whitespace)?; - (name.trim_start().trim_start_matches('*') == asset_name).then(|| hash.to_string()) - }) - .unwrap_or_else(|| panic!("SHA256SUMS.txt does not contain {asset_name}")) + let mut matches = sums.lines().filter_map(|line| { + let (hash, name) = line.split_once(char::is_whitespace)?; + (name + .trim_start() + .strip_prefix('*') + .unwrap_or(name.trim_start()) + == asset_name) + .then_some(hash) + }); + let hash = matches + .next() + .unwrap_or_else(|| panic!("SHA256SUMS.txt does not contain {asset_name}")); + assert!( + matches.next().is_none(), + "SHA256SUMS.txt contains duplicate {asset_name}" + ); + assert!( + hash.len() == 64 && hash.bytes().all(|c| c.is_ascii_hexdigit()), + "SHA256SUMS.txt contains invalid SHA-256 for {asset_name}" + ); + hash.to_ascii_lowercase() } #[derive(Clone, Copy)] @@ -794,26 +920,10 @@ fn cached_download( expected_hash: &str, cache_dir: &Option, ) -> Vec { - if let Some(dir) = cache_dir { - let cached_path = dir.join(cache_key); - if cached_path.is_file() { - match std::fs::read(&cached_path) { - Ok(data) if verify_hash(&data, expected_hash) => { - // Silent cache hit — nothing to surface. - return data; - } - Ok(_) => { - println!("cargo:warning=Cached archive hash mismatch, re-downloading"); - let _ = std::fs::remove_file(&cached_path); - } - Err(e) => { - println!( - "cargo:warning=Failed to read cache {}, re-downloading: {e}", - cached_path.display() - ); - } - } - } + if let Some(dir) = cache_dir + && let Some(data) = read_verified_cache(&dir.join(cache_key), expected_hash) + { + return data; } println!("cargo:warning=Downloading {url}"); @@ -846,6 +956,25 @@ fn cached_download( data } +fn read_verified_cache(path: &Path, expected_hash: &str) -> Option> { + if path.is_file() { + match std::fs::read(path) { + Ok(data) if verify_hash(&data, expected_hash) => return Some(data), + Ok(_) => { + println!("cargo:warning=Cached archive hash mismatch, re-downloading"); + let _ = std::fs::remove_file(path); + } + Err(e) => { + println!( + "cargo:warning=Failed to read cache {}, re-downloading: {e}", + path.display() + ); + } + } + } + None +} + /// Maximum retries after the initial HTTP attempt for transient errors. const MAX_RETRIES: u32 = 5; @@ -981,3 +1110,7 @@ fn verify_hash(data: &[u8], expected: &str) -> bool { hasher.update(data); format!("{:x}", hasher.finalize()) == expected } + +#[cfg(test)] +#[path = "in_process/tests.rs"] +mod tests; diff --git a/rust/build/in_process/tests.rs b/rust/build/in_process/tests.rs new file mode 100644 index 0000000000..89a5c5f5e5 --- /dev/null +++ b/rust/build/in_process/tests.rs @@ -0,0 +1,575 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +#![cfg(test)] + +use std::fs; +use std::io::{Cursor, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use sha2::{Digest, Sha256}; +use tempfile::TempDir; + +use super::*; + +const VERSION: &str = "1.2.3-unstable.20260923"; +const HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const PLATFORMS: &[&str] = &[ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "linuxmusl-arm64", + "linuxmusl-x64", + "win32-arm64", + "win32-x64", +]; + +fn release(sdk: bool) -> Release { + Release::new( + VERSION.into(), + sdk.then_some(format!( + "https://github.com/github/copilot-sdk/releases/download/runtime-{VERSION}" + )) + .as_deref(), + ) + .unwrap() +} + +#[test] +fn legacy_and_unstable_snapshots_resolve_every_asset_and_checksums_consistently() { + for sdk in [false, true] { + let release = release(sdk); + let base = if sdk { + format!("https://github.com/github/copilot-sdk/releases/download/runtime-{VERSION}") + } else { + format!("https://github.com/github/copilot-cli/releases/download/v{VERSION}") + }; + let metadata = if sdk { + format!("release-url={base}\r\n") + } else { + String::new() + }; + assert_eq!( + release.asset_url("SHA256SUMS.txt"), + format!("{base}/SHA256SUMS.txt") + ); + for target in PLATFORMS { + let extension = if target.starts_with("win32") { + "zip" + } else { + "tar.gz" + }; + let cli_asset = format!("copilot-{target}.{extension}"); + let runtime_asset = format!("github-copilot-{VERSION}-{target}.tgz"); + for (key, asset) in [ + (cli_asset.clone(), cli_asset), + (format!("copilot-{target}"), runtime_asset), + ] { + let snapshot = format!( + "# fixture\r\n\r\nversion={VERSION}\r\n{metadata}{key}={}\r\n", + HASH.to_ascii_uppercase() + ); + let (parsed, hash) = parse_snapshot(&snapshot, &key).unwrap(); + assert_eq!(parsed, release); + assert_eq!(hash, HASH); + assert_eq!(parsed.asset_url(&asset), format!("{base}/{asset}")); + let expected_key = if sdk { + format!("copilot-sdk-runtime-{VERSION}-{asset}") + } else { + format!("v{VERSION}-{asset}") + }; + assert_eq!(parsed.cache_key(&asset), expected_key); + assert_eq!( + find_sha256_for_asset(&format!("{HASH} *{asset}\r\n"), &asset), + HASH + ); + } + } + } +} + +#[test] +fn explicit_legacy_url_and_omitted_url_have_the_same_identity() { + for version in [ + "1.2.3", + "1.2.3-4", + "0.0.0-dev", + "1.2.3-unstable.r123.gabcdef0", + "1.2.3-4.unstable.r123.gabcdef0", + ] { + let implicit = Release::new(version.into(), None).unwrap(); + let explicit = Release::new( + version.into(), + Some(&format!( + "https://github.com/github/copilot-cli/releases/download/v{version}" + )), + ) + .unwrap(); + assert_eq!(implicit, explicit); + assert_eq!(implicit.cache_identity(), version); + } + assert_ne!( + release(false).cache_identity(), + release(true).cache_identity() + ); +} + +#[test] +fn source_checkouts_without_snapshots_resolve_canonical_unstable_releases() { + let root = fixture_dir(); + let manifest_dir = root.path().join("rust"); + let node_dir = root.path().join("nodejs"); + fs::create_dir_all(&manifest_dir).unwrap(); + fs::create_dir_all(&node_dir).unwrap(); + assert!(!manifest_dir.join("cli-version.txt").exists()); + assert!(!manifest_dir.join("cli-version-in-process.txt").exists()); + + for (version, sdk_release) in [ + ("1.2.3", false), + ("1.2.3-4", false), + ("0.0.0-dev", false), + ("1.2.3-unstable.r123.gabcdef0", true), + ("1.2.3-4.unstable.r123.gabcdef0", true), + ("0.0.0-0.unstable.r1.g0000000", true), + ("1.2.3-unstable.20260923", false), + ("1.2.3.unstable.r123.gabcdef0", false), + ("1.2.3-unstable.r0.gabcdef0", false), + ("1.2.3-unstable.r0123.gabcdef0", false), + ("01.2.3-unstable.r123.gabcdef0", false), + ("1.2.3-04.unstable.r123.gabcdef0", false), + ("1.2.3-unstable.r123.gabcdef00", false), + ("1.2.3-unstable.r123.gABCDEF0", false), + ] { + fs::write( + node_dir.join("package.json"), + serde_json::json!({ "copilotCliVersion": version }).to_string(), + ) + .unwrap(); + let (release, hash) = resolve_version_and_optional_hash(&manifest_dir, "copilot-linux-x64"); + assert_eq!(release.version, version); + assert_eq!(hash, None); + let base = if sdk_release { + format!("https://github.com/github/copilot-sdk/releases/download/runtime-{version}") + } else { + format!("https://github.com/github/copilot-cli/releases/download/v{version}") + }; + for asset in [ + "SHA256SUMS.txt".into(), + "copilot-linux-x64.tar.gz".into(), + format!("github-copilot-{version}-linux-x64.tgz"), + ] { + assert_eq!(release.asset_url(&asset), format!("{base}/{asset}")); + let key = if sdk_release { + format!("copilot-sdk-runtime-{version}-{asset}") + } else { + format!("v{version}-{asset}") + }; + assert_eq!(release.cache_key(&asset), key); + } + } +} + +#[test] +fn source_checkouts_still_prefer_snapshots_and_preserve_legacy_locations() { + let root = fixture_dir(); + let manifest_dir = root.path().join("rust"); + let node_dir = root.path().join("nodejs"); + fs::create_dir_all(&manifest_dir).unwrap(); + fs::create_dir_all(&node_dir).unwrap(); + fs::write( + node_dir.join("package.json"), + r#"{"copilotCliVersion":"9.9.9"}"#, + ) + .unwrap(); + let version = "1.2.3-4.unstable.r123.gabcdef0"; + for sdk_release in [false, true] { + let base = if sdk_release { + format!("https://github.com/github/copilot-sdk/releases/download/runtime-{version}") + } else { + format!("https://github.com/github/copilot-cli/releases/download/v{version}") + }; + let location = if sdk_release { + format!("release-url={base}\n") + } else { + String::new() + }; + fs::write( + manifest_dir.join("cli-version-in-process.txt"), + format!("version={version}\n{location}copilot-linux-x64={HASH}\n"), + ) + .unwrap(); + let (release, hash) = resolve_version_and_optional_hash(&manifest_dir, "copilot-linux-x64"); + assert_eq!(release.version, version); + assert_eq!(hash.as_deref(), Some(HASH)); + assert_eq!( + release.asset_url("SHA256SUMS.txt"), + format!("{base}/SHA256SUMS.txt") + ); + } +} + +#[test] +fn snapshots_reject_mismatched_versions_untrusted_locations_and_invalid_hashes() { + for url in [ + "https://github.com/github/copilot-sdk/releases/download/runtime-9.9.9", + "https://github.com/github/copilot-sdk/releases/latest", + "https://github.com/github/copilot-agent-runtime/releases/download/runtime-1.2.3", + "https://token@github.com/github/copilot-sdk/releases/download/runtime-1.2.3", + "https://github.com/github/copilot-cli/releases/download/v1.2.3/", + "https://github.com/github/copilot-cli/releases/download/v1.2.3?token=secret", + "http://github.com/github/copilot-cli/releases/download/v1.2.3", + ] { + let snapshot = format!("version=1.2.3\nrelease-url={url}\nasset={HASH}"); + assert!(parse_snapshot(&snapshot, "asset").is_err(), "{url}"); + } + for snapshot in [ + format!("version=\nasset={HASH}"), + format!("version=../1.2.3\nasset={HASH}"), + format!("version=1.2.3\nversion=1.2.4\nasset={HASH}"), + format!("version=1.2.3\nasset={HASH}\nasset={HASH}"), + format!("version=1.2.3\nrelease-url=\nrelease-url=\nasset={HASH}"), + format!("version=1.2.3\nother={HASH}"), + "version=1.2.3\nasset=bad-hash".into(), + format!("version=1.2.3\nmalformed line\nasset={HASH}"), + ] { + assert!(parse_snapshot(&snapshot, "asset").is_err(), "{snapshot}"); + } +} + +#[test] +fn checksum_lookup_rejects_missing_duplicate_and_invalid_entries() { + for sums in [ + format!("{HASH} asset-other\n"), + format!("{HASH} asset\n{HASH} *asset\n"), + "invalid asset\n".into(), + ] { + assert!(std::panic::catch_unwind(|| find_sha256_for_asset(&sums, "asset")).is_err()); + } +} + +fn fixture_dir() -> TempDir { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join("acquisition-tests"); + fs::create_dir_all(&root).unwrap(); + tempfile::Builder::new() + .prefix("fixture-") + .tempdir_in(root) + .unwrap() +} + +#[test] +fn cache_hits_are_verified_and_corruption_is_evicted() { + let fixture = fixture_dir(); + let bytes = b"verified release archive"; + let hash = format!("{:x}", Sha256::digest(bytes)); + let path = fixture.path().join(release(true).cache_key("asset")); + assert_eq!(read_verified_cache(&path, &hash), None); + fs::write(&path, bytes).unwrap(); + assert_eq!( + read_verified_cache(&path, &hash).as_deref(), + Some(bytes.as_slice()) + ); + fs::write(&path, b"corrupt release archive").unwrap(); + assert_eq!(read_verified_cache(&path, &hash), None); + assert!(!path.exists()); +} + +fn tar_archive(files: &[(&str, &[u8])]) -> Vec { + let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + let mut archive = tar::Builder::new(encoder); + for (name, bytes) in files { + append_archive_file(&mut archive, name, bytes, 0o755); + } + archive.into_inner().unwrap().finish().unwrap() +} + +struct Fixture { + dir: TempDir, + platform: Platform, + release: Release, +} + +impl Fixture { + fn new(sdk: bool, windows: bool) -> Self { + let fixture = Self { + dir: fixture_dir(), + platform: Platform { + package_name: if windows { + "copilot-win32-x64" + } else { + "copilot-linux-x64" + }, + binary_name: if windows { "copilot.exe" } else { "copilot" }, + }, + release: release(sdk), + }; + for name in ["out", "cache", "extracted"] { + fs::create_dir_all(fixture.path(name)).unwrap(); + } + let cli = if windows { + let mut archive = zip::ZipWriter::new(Cursor::new(Vec::new())); + archive + .start_file("copilot.exe", zip::write::SimpleFileOptions::default()) + .unwrap(); + archive.write_all(b"MZ fixture CLI").unwrap(); + archive.finish().unwrap().into_inner() + } else { + tar_archive(&[("copilot", b"fixture CLI")]) + }; + let target = fixture + .platform + .package_name + .strip_prefix("copilot-") + .unwrap(); + let runtime = tar_archive(&[ + ( + &format!("package/{}", fixture.platform.runtime_wrapper_name()), + b"fixture wrapper", + ), + ( + &format!("package/prebuilds/{target}/runtime.node"), + b"fixture native library", + ), + ("package/tls/roots.pem", b"fixture support file"), + ]); + for (snapshot, key, asset, bytes) in [ + ( + "cli-version.txt", + fixture.platform.cli_asset_name(), + fixture.platform.cli_asset_name(), + cli, + ), + ( + "cli-version-in-process.txt", + fixture.platform.package_name.into(), + format!("github-copilot-{VERSION}-{target}.tgz"), + runtime, + ), + ] { + let location = if sdk { + format!( + "release-url=https://github.com/github/copilot-sdk/releases/download/runtime-{VERSION}\n" + ) + } else { + String::new() + }; + fs::write( + fixture.path(snapshot), + format!( + "version={VERSION}\n{location}{key}={:x}\n", + Sha256::digest(&bytes) + ), + ) + .unwrap(); + fs::write( + fixture + .path("cache") + .join(fixture.release.cache_key(&asset)), + bytes, + ) + .unwrap(); + } + fixture + } + + fn path(&self, path: &str) -> PathBuf { + self.dir.path().join(path) + } + + fn command(&self, bundled: bool, in_process: bool) -> Command { + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args([ + "--exact", + "implementation::tests::run_build_script", + "--nocapture", + ]) + .env("COPILOT_ACQUISITION_TEST_CHILD", "1") + .env("CARGO_MANIFEST_DIR", self.dir.path()) + .env("OUT_DIR", self.path("out")) + .env("BUNDLED_CLI_CACHE_DIR", self.path("cache")) + .env("COPILOT_CLI_EXTRACT_DIR", self.path("extracted")) + .env( + "CARGO_CFG_TARGET_OS", + if self.platform.package_name.contains("win32") { + "windows" + } else { + "linux" + }, + ) + .env("CARGO_CFG_TARGET_ARCH", "x86_64") + .env("CARGO_CFG_TARGET_ENV", "") + .env("CARGO_FEATURE_RUNTIME", "1") + .env_remove("COPILOT_SKIP_CLI_DOWNLOAD") + .env_remove("DOCS_RS") + .env_remove("CARGO_FEATURE_LOCAL_RUNTIME") + .env_remove("CARGO_FEATURE_BUNDLED_CLI") + .env_remove("CARGO_FEATURE_IN_PROCESS"); + if bundled { + command.env("CARGO_FEATURE_BUNDLED_CLI", "1"); + } + if in_process { + command.env("CARGO_FEATURE_IN_PROCESS", "1"); + } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + command + } +} + +#[track_caller] +fn succeeded(output: Output) -> String { + assert!( + output.status.success(), + "stdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(!stdout.contains("Downloading "), "{stdout}"); + stdout +} + +#[test] +fn run_build_script() { + if std::env::var_os("COPILOT_ACQUISITION_TEST_CHILD").is_some() { + super::main(); + } +} + +#[test] +fn seeded_builds_support_bundled_in_process_and_extracted_modes_for_both_locations() { + for sdk in [false, true] { + for windows in [false, true] { + for bundled in [false, true] { + for in_process in [false, true] { + let fixture = Fixture::new(sdk, windows); + let stdout = succeeded(fixture.command(bundled, in_process).output().unwrap()); + assert!(stdout.contains(&format!( + "cargo:rustc-env=COPILOT_SDK_CLI_CACHE_ID={}", + fixture.release.cache_identity() + ))); + if bundled { + assert!(stdout.contains("cargo:rustc-cfg=has_bundled_cli")); + assert!(fixture.path("out/copilot_cli.archive").is_file()); + let runtime = + fs::read(fixture.path("out/copilot_runtime.archive")).unwrap(); + assert!(archive_contains_tar_entry( + &runtime, + fixture.platform.runtime_wrapper_name() + )); + assert!(archive_contains_tar_entry(&runtime, "roots.pem")); + assert_eq!( + archive_contains_tar_entry( + &runtime, + fixture.platform.runtime_library_name() + ), + in_process + ); + } else { + assert!(stdout.contains("cargo:rustc-cfg=has_extracted_cli")); + assert!( + fixture + .path("extracted") + .join(fixture.platform.runtime_wrapper_name()) + .is_file() + ); + assert!(fixture.path("extracted/tls/roots.pem").is_file()); + assert_eq!( + fixture + .path("extracted") + .join(fixture.platform.runtime_library_name()) + .is_file(), + in_process + ); + // A valid extracted cache needs neither archive nor a network lookup. + fs::remove_dir_all(fixture.path("cache")).unwrap(); + succeeded(fixture.command(bundled, in_process).output().unwrap()); + } + } + } + } + } +} + +#[test] +fn stale_extracted_markers_reinstall_verified_runtime_assets() { + let fixture = Fixture::new(true, false); + succeeded(fixture.command(false, true).output().unwrap()); + let wrapper = fixture + .path("extracted") + .join(fixture.platform.runtime_wrapper_name()); + fs::write(&wrapper, b"stale wrapper").unwrap(); + fs::write( + fixture.path("extracted/.hostless-runtime-assets-v1"), + format!("{VERSION}\n{HASH}\n"), + ) + .unwrap(); + fs::write(fixture.path("extracted/stale-file"), b"old payload").unwrap(); + succeeded(fixture.command(false, true).output().unwrap()); + assert_eq!(fs::read(wrapper).unwrap(), b"fixture wrapper"); + assert!(!fixture.path("extracted/stale-file").exists()); +} + +#[test] +fn builds_reject_inconsistent_snapshot_versions_and_locations_before_acquisition() { + for change_version in [false, true] { + let fixture = Fixture::new(true, false); + let path = fixture.path("cli-version.txt"); + let contents = fs::read_to_string(&path).unwrap(); + let changed = if change_version { + contents.replace(VERSION, "9.9.9") + } else { + contents.replace( + &format!("release-url=https://github.com/github/copilot-sdk/releases/download/runtime-{VERSION}\n"), + "" + ) + }; + fs::write(path, changed).unwrap(); + let output = fixture.command(true, false).output().unwrap(); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("same version and release URL")); + assert!(!String::from_utf8_lossy(&output.stdout).contains("Downloading ")); + } +} + +#[test] +fn external_stream_only_builds_need_no_runtime_artifacts() { + let fixture = Fixture::new(true, false); + fs::remove_file(fixture.path("cli-version.txt")).unwrap(); + fs::remove_file(fixture.path("cli-version-in-process.txt")).unwrap(); + fs::remove_dir_all(fixture.path("cache")).unwrap(); + let mut command = fixture.command(false, false); + command.env_remove("CARGO_FEATURE_RUNTIME"); + let stdout = succeeded(command.output().unwrap()); + assert!(!stdout.contains("cargo:rustc-env=COPILOT_SDK_CLI_VERSION")); + assert!(!stdout.contains("cargo:rustc-cfg=has_")); + assert!(!fixture.path("extracted/copilot-runtime").exists()); +} + +#[test] +fn skip_modes_need_no_snapshots_and_local_runtime_preserves_bundled_precedence() { + for skip in [ + "DOCS_RS", + "COPILOT_SKIP_CLI_DOWNLOAD", + "CARGO_FEATURE_LOCAL_RUNTIME", + ] { + let fixture = Fixture::new(true, false); + fs::remove_file(fixture.path("cli-version.txt")).unwrap(); + fs::remove_file(fixture.path("cli-version-in-process.txt")).unwrap(); + fs::remove_dir_all(fixture.path("cache")).unwrap(); + let mut command = fixture.command(false, true); + command.env(skip, "1"); + let stdout = succeeded(command.output().unwrap()); + assert!(!stdout.contains("cargo:rustc-cfg=has_")); + } + let fixture = Fixture::new(true, false); + let mut command = fixture.command(true, true); + command.env("CARGO_FEATURE_LOCAL_RUNTIME", "1"); + assert!(succeeded(command.output().unwrap()).contains("cargo:rustc-cfg=has_bundled_cli")); +} diff --git a/rust/scripts/snapshot-bundled-cli-version.sh b/rust/scripts/snapshot-bundled-cli-version.sh index 0045f5e6c8..6df8cfbdbd 100755 --- a/rust/scripts/snapshot-bundled-cli-version.sh +++ b/rust/scripts/snapshot-bundled-cli-version.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# Copyright (c) Microsoft Corporation. All rights reserved. # # Snapshot the Copilot CLI version + per-platform SHA-256 hashes for the # rust crate's bundled-CLI build.rs. Runs at SDK publish time, mirroring @@ -17,21 +18,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RUST_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${RUST_DIR}/.." && pwd)" -PACKAGE_FILE="${REPO_ROOT}/nodejs/package.json" OUTPUT="${RUST_DIR}/cli-version.txt" - -if [[ ! -f "${PACKAGE_FILE}" ]]; then - echo "error: ${PACKAGE_FILE} not found" >&2 - exit 1 -fi - -VERSION="$(node -e "console.log(require('${PACKAGE_FILE}').copilotCliVersion)")" -if [[ -z "${VERSION}" ]]; then - echo "error: could not read copilotCliVersion from ${PACKAGE_FILE}" >&2 - exit 1 -fi -CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}/SHA256SUMS.txt" -SHA256SUMS="$(curl --fail --silent --show-error --location --retry 3 "${CHECKSUMS_URL}")" +source "${SCRIPT_DIR}/snapshot-release.sh" "$@" ASSETS=( "copilot-darwin-arm64.tar.gz" @@ -50,12 +38,11 @@ trap 'rm -f "${TEMP_OUTPUT}"' EXIT echo "# Auto-generated by rust/scripts/snapshot-bundled-cli-version.sh" echo "# Do not edit. Regenerated by the publish workflow on every release." echo "version=${VERSION}" + if [[ "${RELEASE_URL}" != "${DEFAULT_RELEASE_URL}" ]]; then + echo "release-url=${RELEASE_URL}" + fi for asset in "${ASSETS[@]}"; do - hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v asset="${asset}" '$2 == asset || $2 == "*" asset { print $1; exit }')" - if [[ -z "${hash}" ]]; then - echo "error: SHA256SUMS.txt does not contain ${asset}" >&2 - exit 1 - fi + hash="$(snapshot_hash "${asset}")" echo "${asset}=${hash}" done } > "${TEMP_OUTPUT}" diff --git a/rust/scripts/snapshot-bundled-in-process-version.sh b/rust/scripts/snapshot-bundled-in-process-version.sh index 9fe2298c78..2dd270a1d6 100755 --- a/rust/scripts/snapshot-bundled-in-process-version.sh +++ b/rust/scripts/snapshot-bundled-in-process-version.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# Copyright (c) Microsoft Corporation. All rights reserved. # # Snapshot the Copilot CLI version + per-platform release hashes for the # rust crate's bundled-in-process build path. @@ -8,21 +9,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RUST_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${RUST_DIR}/.." && pwd)" -PACKAGE_FILE="${REPO_ROOT}/nodejs/package.json" OUTPUT="${RUST_DIR}/cli-version-in-process.txt" - -if [[ ! -f "${PACKAGE_FILE}" ]]; then - echo "error: ${PACKAGE_FILE} not found" >&2 - exit 1 -fi - -VERSION="$(node -e "console.log(require('${PACKAGE_FILE}').copilotCliVersion)")" -if [[ -z "${VERSION}" ]]; then - echo "error: could not read copilotCliVersion from ${PACKAGE_FILE}" >&2 - exit 1 -fi -CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}/SHA256SUMS.txt" -SHA256SUMS="$(curl --fail --silent --show-error --location --retry 3 "${CHECKSUMS_URL}")" +source "${SCRIPT_DIR}/snapshot-release.sh" "$@" PACKAGES=( "copilot-darwin-arm64" @@ -41,14 +29,13 @@ trap 'rm -f "${TEMP_OUTPUT}"' EXIT echo "# Auto-generated by rust/scripts/snapshot-bundled-in-process-version.sh" echo "# Do not edit. Regenerated by the publish workflow on every release." echo "version=${VERSION}" + if [[ "${RELEASE_URL}" != "${DEFAULT_RELEASE_URL}" ]]; then + echo "release-url=${RELEASE_URL}" + fi for package in "${PACKAGES[@]}"; do platform="${package#copilot-}" asset="github-copilot-${VERSION}-${platform}.tgz" - hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v asset="${asset}" '$2 == asset || $2 == "*" asset { print $1; exit }')" - if [[ -z "${hash}" ]]; then - echo "error: SHA256SUMS.txt does not contain ${asset}" >&2 - exit 1 - fi + hash="$(snapshot_hash "${asset}")" echo "${package}=${hash}" done } > "${TEMP_OUTPUT}" diff --git a/rust/scripts/snapshot-release.sh b/rust/scripts/snapshot-release.sh new file mode 100644 index 0000000000..37bda6edf9 --- /dev/null +++ b/rust/scripts/snapshot-release.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Copyright (c) Microsoft Corporation. All rights reserved. +# +# Sourced by the two snapshot entry points. + +usage() { + cat < + https://github.com/github/copilot-sdk/releases/download/runtime- + Default: the copilot-cli URL. + --checksums FILE Read local SHA256SUMS.txt instead of downloading it. + --output FILE Write to a selected-source staging file instead of this script's crate. + --help Show this help. + +No environment variables or credentials are required. Local checksums let +packaging run before the public release exists; the snapshot still pins its +final public URL. Both snapshot scripts must use the same version and URL. +EOF +} + +VERSION="" +RELEASE_URL="" +CHECKSUMS_FILE="" +while [[ $# -gt 0 ]]; do + case "$1" in + --help|-h) usage; exit 0 ;; + --version|--release-url|--checksums|--output) + if [[ $# -lt 2 || -z "$2" || "$2" == --* ]]; then + echo "error: $1 requires a value" >&2 + exit 1 + fi + case "$1" in + --version) VERSION="$2" ;; + --release-url) RELEASE_URL="$2" ;; + --checksums) CHECKSUMS_FILE="$2" ;; + --output) OUTPUT="$2" ;; + esac + shift 2 + ;; + *) echo "error: unknown argument: $1" >&2; usage >&2; exit 1 ;; + esac +done + +if [[ -z "${VERSION}" ]]; then + PACKAGE_FILE="${REPO_ROOT}/nodejs/package.json" + if [[ ! -f "${PACKAGE_FILE}" ]]; then + echo "error: ${PACKAGE_FILE} not found" >&2 + exit 1 + fi + VERSION="$(node -e 'console.log(require(process.argv[1]).copilotCliVersion ?? "")' "${PACKAGE_FILE}")" +fi +if [[ ! "${VERSION}" =~ ^[0-9][a-zA-Z0-9.+-]*$ ]]; then + echo "error: invalid runtime version: ${VERSION}" >&2 + exit 1 +fi + +DEFAULT_RELEASE_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}" +RELEASE_URL="${RELEASE_URL:-${DEFAULT_RELEASE_URL}}" +if [[ "${RELEASE_URL}" != "${DEFAULT_RELEASE_URL}" && + "${RELEASE_URL}" != "https://github.com/github/copilot-sdk/releases/download/runtime-${VERSION}" ]]; then + echo "error: --release-url must be the exact public copilot-cli/v or copilot-sdk/runtime- release URL" >&2 + exit 1 +fi + +if [[ -n "${CHECKSUMS_FILE}" ]]; then + SHA256SUMS="$(cat "${CHECKSUMS_FILE}")" +else + SHA256SUMS="$(curl --fail --silent --show-error --location --retry 3 "${RELEASE_URL}/SHA256SUMS.txt")" +fi +# Accept checksum files produced on Windows as well as Unix. +SHA256SUMS="${SHA256SUMS//$'\r'/}" + +snapshot_hash() { + local asset="$1" hash + hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v asset="${asset}" '$2 == asset || $2 == "*" asset { print $1 }')" + if [[ ! "${hash}" =~ ^[a-fA-F0-9]{64}$ ]]; then + echo "error: SHA256SUMS.txt must contain one valid SHA-256 for ${asset}" >&2 + return 1 + fi + printf '%s\n' "${hash}" | tr '[:upper:]' '[:lower:]' +} diff --git a/rust/scripts/snapshot-version.test.mjs b/rust/scripts/snapshot-version.test.mjs new file mode 100644 index 0000000000..29f69cbb8a --- /dev/null +++ b/rust/scripts/snapshot-version.test.mjs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const targets = [ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "linuxmusl-arm64", + "linuxmusl-x64", + "win32-arm64", + "win32-x64", +]; +const scripts = [ + ["snapshot-bundled-cli-version.sh", "cli-version.txt", false], + ["snapshot-bundled-in-process-version.sh", "cli-version-in-process.txt", true], +]; + +function fixture(t, version) { + const root = mkdtempSync(join(scriptsDir, ".snapshot-test-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + const rust = join(root, "rust"); + mkdirSync(join(rust, "scripts"), { recursive: true }); + mkdirSync(join(root, "nodejs")); + mkdirSync(join(root, "bin")); + writeFileSync(join(root, "nodejs", "package.json"), JSON.stringify({ copilotCliVersion: version })); + for (const script of [...scripts.map(([script]) => script), "snapshot-release.sh"]) { + copyFileSync(join(scriptsDir, script), join(rust, "scripts", script)); + } + const hashes = new Map(); + for (const target of targets) { + for (const asset of [ + `copilot-${target}.${target.startsWith("win32") ? "zip" : "tar.gz"}`, + `github-copilot-${version}-${target}.tgz`, + ]) { + hashes.set(asset, createHash("sha256").update(asset).digest("hex")); + } + } + const sums = [...hashes].map(([asset, hash]) => `${hash.toUpperCase()} *${asset}`).join("\r\n"); + writeFileSync(join(rust, "SHA256SUMS.txt"), `${sums}\r\n`); + // Capture the download boundary without contacting GitHub. + writeFileSync(join(root, "bin", "curl"), `#!/usr/bin/env bash +printf '%s\\n' "$@" > curl-args.txt +cat SHA256SUMS.txt +`, { mode: 0o755 }); + return { + rust, + hashes, + run(script, args = []) { + const result = spawnSync("bash", [ + "-c", + 'export PATH="$PWD/../bin:$PATH"; exec bash "$@"', + "snapshot-test", + join("scripts", script), + ...args, + ], { + cwd: rust, + encoding: "utf8", + windowsHide: true, + timeout: 30_000, + }); + assert.ifError(result.error); + return result; + }, + }; +} + +function assertSnapshot(fixture, filename, version, runtime, releaseUrl) { + const contents = readFileSync(join(fixture.rust, filename), "utf8"); + const entries = new Map(contents.split(/\r?\n/).filter((line) => line && !line.startsWith("#")) + .map((line) => line.split("="))); + assert.equal(entries.get("version"), version); + assert.equal(entries.get("release-url"), releaseUrl); + assert.equal(entries.size, targets.length + 1 + Number(Boolean(releaseUrl))); + for (const target of targets) { + const key = runtime ? `copilot-${target}` : `copilot-${target}.${target.startsWith("win32") ? "zip" : "tar.gz"}`; + const asset = runtime ? `github-copilot-${version}-${target}.tgz` : key; + assert.equal(entries.get(key), fixture.hashes.get(asset)); + } +} + +test("no-option snapshots keep the legacy exact version, URL, and hash format", (t) => { + const version = "1.2.3-4"; + const f = fixture(t, version); + for (const [script, output, runtime] of scripts) { + const result = f.run(script); + assert.equal(result.status, 0, result.stderr); + assertSnapshot(f, output, version, runtime, undefined); + assert.match( + readFileSync(join(f.rust, "curl-args.txt"), "utf8"), + /https:\/\/github\.com\/github\/copilot-cli\/releases\/download\/v1\.2\.3-4\/SHA256SUMS\.txt/, + ); + } +}); + +test("local checksum snapshots pin the final unstable URL without publication or sibling metadata", (t) => { + const version = "1.2.3-unstable.20260923"; + const url = `https://github.com/github/copilot-sdk/releases/download/runtime-${version}`; + const f = fixture(t, version); + rmSync(join(f.rust, "..", "nodejs"), { recursive: true }); + for (const [script, output, runtime] of scripts) { + const result = f.run(script, ["--version", version, "--release-url", url, "--checksums", "SHA256SUMS.txt"]); + assert.equal(result.status, 0, result.stderr); + assertSnapshot(f, output, version, runtime, url); + } + assert.throws(() => readFileSync(join(f.rust, "curl-args.txt")), { code: "ENOENT" }); +}); + +test("URL-only override fetches checksums from the exact selected release", (t) => { + const version = "1.2.3-unstable.20260923"; + const url = `https://github.com/github/copilot-sdk/releases/download/runtime-${version}`; + const f = fixture(t, version); + for (const [script, output, runtime] of scripts) { + const result = f.run(script, ["--release-url", url]); + assert.equal(result.status, 0, result.stderr); + assertSnapshot(f, output, version, runtime, url); + assert.equal(readFileSync(join(f.rust, "curl-args.txt"), "utf8").trim().split("\n").at(-1), `${url}/SHA256SUMS.txt`); + } +}); + +test("reviewed producers can stamp normal promotions without changing legacy product code", (t) => { + for (const version of ["1.2.3", "1.2.3-4"]) { + const f = fixture(t, version); + const product = join(f.rust, "..", "old-product", "rust"); + mkdirSync(product, { recursive: true }); + writeFileSync(join(product, "build.rs"), "// unchanged legacy build script\n"); + rmSync(join(f.rust, "..", "nodejs"), { recursive: true }); + for (const [script, output, runtime] of scripts) { + writeFileSync(join(f.rust, output), "reviewed snapshot must remain unchanged"); + const result = f.run(script, [ + "--version", version, + "--release-url", `https://github.com/github/copilot-cli/releases/download/v${version}`, + "--checksums", "SHA256SUMS.txt", + "--output", join(product, output), + ]); + assert.equal(result.status, 0, result.stderr); + assertSnapshot({ ...f, rust: product }, output, version, runtime, undefined); + assert.equal(readFileSync(join(f.rust, output), "utf8"), "reviewed snapshot must remain unchanged"); + } + assert.equal(readFileSync(join(product, "build.rs"), "utf8"), "// unchanged legacy build script\n"); + assert.throws(() => readFileSync(join(product, "scripts", "snapshot-release.sh")), { code: "ENOENT" }); + assert.throws(() => readFileSync(join(f.rust, "curl-args.txt")), { code: "ENOENT" }); + } +}); + +test("invalid or mismatched release locations fail before acquisition", (t) => { + const f = fixture(t, "1.2.3"); + for (const [script] of scripts) { + for (const url of [ + "https://github.com/github/copilot-sdk/releases/download/runtime-9.9.9", + "https://github.com/github/copilot-cli/releases/latest", + "https://user:token@github.com/github/copilot-sdk/releases/download/runtime-1.2.3", + ]) { + const result = f.run(script, ["--release-url", url]); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /exact public/); + } + } + assert.throws(() => readFileSync(join(f.rust, "curl-args.txt")), { code: "ENOENT" }); +}); + +test("missing, duplicate, and invalid hashes preserve an existing snapshot", (t) => { + const f = fixture(t, "1.2.3"); + for (const [script, output, runtime] of scripts) { + const asset = runtime ? "github-copilot-1.2.3-darwin-arm64.tgz" : "copilot-darwin-arm64.tar.gz"; + const hash = f.hashes.get(asset); + for (const contents of ["", `bad ${asset}\n`, `${hash} ${asset}\n${hash} ${asset}\n`]) { + writeFileSync(join(f.rust, output), "original snapshot"); + writeFileSync(join(f.rust, "invalid-sums.txt"), contents); + const result = f.run(script, ["--checksums", "invalid-sums.txt"]); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /one valid SHA-256/); + assert.equal(readFileSync(join(f.rust, output), "utf8"), "original snapshot"); + } + } +}); diff --git a/rust/src/ahp_host.rs b/rust/src/ahp_host.rs index c3e9fc27cf..2b42b86ff0 100644 --- a/rust/src/ahp_host.rs +++ b/rust/src/ahp_host.rs @@ -958,6 +958,7 @@ fn resume_config_from_host( "sessionId" => {}, "continuePendingWork" => config.continue_pending_work = serde_json::from_value(value.clone())?, "suppressResumeEvent" => config.suppress_resume_event = serde_json::from_value(value.clone())?, + "allowTranscriptRecovery" => config.allow_transcript_recovery = serde_json::from_value(value.clone())?, $($name => config.$field = serde_json::from_value(value.clone())?,)* _ => return Err(handoff_error("Unsupported AHP resume configuration setting")), } @@ -988,6 +989,9 @@ pub(crate) fn resume_config_for_host(config: &ResumeSessionConfig) -> Result String { } #[cfg(test)] -mod tests { - use std::ffi::OsStr; - use std::fs; - use std::path::{Path, PathBuf}; - - use tempfile::tempdir; - - use super::{CLI_CACHE_DIR, cache_install_dir, runtime_install_dir}; - - #[test] - fn custom_runtime_directory_is_used_directly() { - let cache = Path::new("ignored-cache"); - let custom = OsStr::new("custom-runtime"); - - assert_eq!( - runtime_install_dir(Some(custom), cache, "1.2.3"), - PathBuf::from(custom) - ); - } - - #[test] - fn stale_runtime_cleanup_cannot_remove_same_version_bundled_cli() { - let cache = tempdir().expect("create cache root"); - let version = "1.2.3/test"; - let bundled_cli_dir = cache_install_dir(cache.path(), CLI_CACHE_DIR, version); - let runtime_dir = runtime_install_dir(None, cache.path(), version); - let bundled_cli = bundled_cli_dir.join(if cfg!(windows) { - "copilot.exe" - } else { - "copilot" - }); - let runtime_marker = runtime_dir.join(".hostless-runtime-assets-v1"); - - fs::create_dir_all(&bundled_cli_dir).expect("create bundled CLI directory"); - fs::write(&bundled_cli, b"bundled-cli").expect("write bundled CLI"); - fs::create_dir_all(&runtime_dir).expect("create runtime directory"); - fs::write(&runtime_marker, b"stale").expect("write stale runtime marker"); - - assert_ne!(runtime_dir, bundled_cli_dir); - fs::remove_dir_all(&runtime_dir).expect("clear stale runtime directory"); - - assert_eq!( - fs::read(&bundled_cli).expect("bundled CLI survives runtime cleanup"), - b"bundled-cli" - ); - assert!(!runtime_marker.exists()); - } -} +#[path = "cache_paths/tests.rs"] +mod tests; diff --git a/rust/src/cache_paths/tests.rs b/rust/src/cache_paths/tests.rs new file mode 100644 index 0000000000..3e165d40c4 --- /dev/null +++ b/rust/src/cache_paths/tests.rs @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; + +use tempfile::tempdir; + +use super::{CLI_CACHE_DIR, cache_install_dir, runtime_install_dir}; + +#[test] +fn custom_runtime_directory_is_used_directly() { + let cache = Path::new("ignored-cache"); + let custom = OsStr::new("custom-runtime"); + + assert_eq!( + runtime_install_dir(Some(custom), cache, "1.2.3"), + PathBuf::from(custom) + ); +} + +#[test] +fn stale_runtime_cleanup_cannot_remove_same_version_bundled_cli() { + let cache = tempdir().expect("create cache root"); + let version = "1.2.3/test"; + let bundled_cli_dir = cache_install_dir(cache.path(), CLI_CACHE_DIR, version); + let runtime_dir = runtime_install_dir(None, cache.path(), version); + let bundled_cli = bundled_cli_dir.join(if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + }); + let runtime_marker = runtime_dir.join(".hostless-runtime-assets-v1"); + + fs::create_dir_all(&bundled_cli_dir).expect("create bundled CLI directory"); + fs::write(&bundled_cli, b"bundled-cli").expect("write bundled CLI"); + fs::create_dir_all(&runtime_dir).expect("create runtime directory"); + fs::write(&runtime_marker, b"stale").expect("write stale runtime marker"); + + assert_ne!(runtime_dir, bundled_cli_dir); + fs::remove_dir_all(&runtime_dir).expect("clear stale runtime directory"); + + assert_eq!( + fs::read(&bundled_cli).expect("bundled CLI survives runtime cleanup"), + b"bundled-cli" + ); + assert!(!runtime_marker.exists()); +} diff --git a/rust/src/canvas.rs b/rust/src/canvas.rs index ddb92a11e6..5768f00112 100644 --- a/rust/src/canvas.rs +++ b/rust/src/canvas.rs @@ -154,6 +154,12 @@ pub trait CanvasHandler: Send + Sync { ) -> CanvasResult; /// Handle a non-lifecycle action declared by the canvas. + /// + /// The returned value is sent to the model as the `invoke_canvas_action` + /// tool result. To return text and images, as tool handlers do, serialize a + /// [`ToolResultExpanded`](crate::types::ToolResultExpanded) (with + /// `binary_results_for_llm`) into the returned value; any other value is + /// rendered to the model as JSON text. async fn on_action( &self, _ctx: crate::generated::api_types::CanvasProviderInvokeActionRequest, @@ -171,130 +177,4 @@ pub trait CanvasHandler: Send + Sync { } #[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - use crate::generated::api_types::{ - CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - }; - use crate::types::SessionId; - - struct EchoHandler; - - #[async_trait] - impl CanvasHandler for EchoHandler { - async fn on_open( - &self, - ctx: CanvasProviderOpenRequest, - ) -> CanvasResult { - Ok(CanvasProviderOpenResult { - url: Some(format!("https://example.test/{}", ctx.canvas_id)), - title: Some("Echo".to_string()), - status: Some("ready".to_string()), - }) - } - - async fn on_action(&self, ctx: CanvasProviderInvokeActionRequest) -> CanvasResult { - Ok(json!({ "echoed": ctx.action_name, "input": ctx.input })) - } - } - - #[test] - fn declaration_serializes_camel_case_and_skips_none() { - let decl = CanvasDeclaration { - id: "counter".to_string(), - display_name: "Counter".to_string(), - description: "Count things".to_string(), - input_schema: None, - actions: Some(vec![CanvasAction { - name: "increment".to_string(), - description: Some("bump".to_string()), - input_schema: None, - }]), - }; - - let value = serde_json::to_value(&decl).unwrap(); - - assert_eq!(value["id"], "counter"); - assert_eq!(value["displayName"], "Counter"); - assert_eq!(value["description"], "Count things"); - assert_eq!(value["actions"][0]["name"], "increment"); - } - - #[tokio::test] - async fn handler_on_open_returns_response() { - let handler = EchoHandler; - let response = handler - .on_open(CanvasProviderOpenRequest { - session_id: SessionId::from("s1"), - extension_id: "project:echo".to_string(), - canvas_id: "echo".to_string(), - instance_id: "echo-1".to_string(), - input: Some(json!({ "x": 1 })), - host: None, - session: None, - }) - .await - .unwrap(); - - assert_eq!(response.url.as_deref(), Some("https://example.test/echo")); - assert_eq!(response.title.as_deref(), Some("Echo")); - assert_eq!(response.status.as_deref(), Some("ready")); - } - - #[tokio::test] - async fn handler_on_action_returns_value() { - let handler = EchoHandler; - let result = handler - .on_action(CanvasProviderInvokeActionRequest { - session_id: SessionId::from("s1"), - extension_id: "project:echo".to_string(), - canvas_id: "echo".to_string(), - instance_id: "inst-1".to_string(), - action_name: "shout".to_string(), - input: Some(json!("hi")), - host: None, - session: None, - }) - .await - .unwrap(); - - assert_eq!(result["echoed"], "shout"); - assert_eq!(result["input"], "hi"); - } - - #[tokio::test] - async fn default_on_action_returns_no_handler_error() { - struct OpenOnly; - #[async_trait] - impl CanvasHandler for OpenOnly { - async fn on_open( - &self, - _ctx: CanvasProviderOpenRequest, - ) -> CanvasResult { - Ok(CanvasProviderOpenResult { - url: None, - title: None, - status: None, - }) - } - } - - let err = OpenOnly - .on_action(CanvasProviderInvokeActionRequest { - session_id: SessionId::from("s1"), - extension_id: "project:open-only".to_string(), - canvas_id: "x".to_string(), - instance_id: "x-1".to_string(), - action_name: "anything".to_string(), - input: Some(Value::Null), - host: None, - session: None, - }) - .await - .unwrap_err(); - - assert_eq!(err.code, "canvas_action_no_handler"); - } -} +mod tests; diff --git a/rust/src/canvas/tests.rs b/rust/src/canvas/tests.rs new file mode 100644 index 0000000000..339f1439b9 --- /dev/null +++ b/rust/src/canvas/tests.rs @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use serde_json::json; + +use super::*; +use crate::generated::api_types::{ + CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, +}; +use crate::types::SessionId; + +struct EchoHandler; + +#[async_trait] +impl CanvasHandler for EchoHandler { + async fn on_open( + &self, + ctx: CanvasProviderOpenRequest, + ) -> CanvasResult { + Ok(CanvasProviderOpenResult { + url: Some(format!("https://example.test/{}", ctx.canvas_id)), + title: Some("Echo".to_string()), + status: Some("ready".to_string()), + }) + } + + async fn on_action(&self, ctx: CanvasProviderInvokeActionRequest) -> CanvasResult { + Ok(json!({ "echoed": ctx.action_name, "input": ctx.input })) + } +} + +#[test] +fn declaration_serializes_camel_case_and_skips_none() { + let decl = CanvasDeclaration { + id: "counter".to_string(), + display_name: "Counter".to_string(), + description: "Count things".to_string(), + input_schema: None, + actions: Some(vec![CanvasAction { + name: "increment".to_string(), + description: Some("bump".to_string()), + input_schema: None, + }]), + }; + + let value = serde_json::to_value(&decl).unwrap(); + + assert_eq!(value["id"], "counter"); + assert_eq!(value["displayName"], "Counter"); + assert_eq!(value["description"], "Count things"); + assert_eq!(value["actions"][0]["name"], "increment"); +} + +#[tokio::test] +async fn handler_on_open_returns_response() { + let handler = EchoHandler; + let response = handler + .on_open(CanvasProviderOpenRequest { + session_id: SessionId::from("s1"), + extension_id: "project:echo".to_string(), + canvas_id: "echo".to_string(), + instance_id: "echo-1".to_string(), + input: Some(json!({ "x": 1 })), + host: None, + session: None, + }) + .await + .unwrap(); + + assert_eq!(response.url.as_deref(), Some("https://example.test/echo")); + assert_eq!(response.title.as_deref(), Some("Echo")); + assert_eq!(response.status.as_deref(), Some("ready")); +} + +#[tokio::test] +async fn handler_on_action_returns_value() { + let handler = EchoHandler; + let result = handler + .on_action(CanvasProviderInvokeActionRequest { + session_id: SessionId::from("s1"), + extension_id: "project:echo".to_string(), + canvas_id: "echo".to_string(), + instance_id: "inst-1".to_string(), + action_name: "shout".to_string(), + input: Some(json!("hi")), + host: None, + session: None, + }) + .await + .unwrap(); + + assert_eq!(result["echoed"], "shout"); + assert_eq!(result["input"], "hi"); +} + +#[tokio::test] +async fn default_on_action_returns_no_handler_error() { + struct OpenOnly; + #[async_trait] + impl CanvasHandler for OpenOnly { + async fn on_open( + &self, + _ctx: CanvasProviderOpenRequest, + ) -> CanvasResult { + Ok(CanvasProviderOpenResult { + url: None, + title: None, + status: None, + }) + } + } + + let err = OpenOnly + .on_action(CanvasProviderInvokeActionRequest { + session_id: SessionId::from("s1"), + extension_id: "project:open-only".to_string(), + canvas_id: "x".to_string(), + instance_id: "x-1".to_string(), + action_name: "anything".to_string(), + input: Some(Value::Null), + host: None, + session: None, + }) + .await + .unwrap_err(); + + assert_eq!(err.code, "canvas_action_no_handler"); +} diff --git a/rust/src/copilot_request_handler/http_response_reader.rs b/rust/src/copilot_request_handler/http_response_reader.rs index 96a5946a2a..d8bffad9b2 100644 --- a/rust/src/copilot_request_handler/http_response_reader.rs +++ b/rust/src/copilot_request_handler/http_response_reader.rs @@ -92,180 +92,4 @@ impl HttpResponseReader { } #[cfg(test)] -mod tests { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::time::Duration; - - use futures_util::stream; - - use super::*; - - #[tokio::test] - async fn preserves_empty_large_fragmented_and_partial_bodies() { - for size in [0, 1, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE * 3 + 7] { - let expected: Vec = (0..size).map(|i| (i % 256) as u8).collect(); - for fragment_size in [1, 1024, CHUNK_SIZE, CHUNK_SIZE * 4] { - let fragments: Vec<_> = expected - .chunks(fragment_size) - .map(Bytes::copy_from_slice) - .collect(); - let body = stream::iter( - [Bytes::new()] - .into_iter() - .chain(fragments) - .chain([Bytes::new()]) - .map(Ok), - ); - let mut reader = HttpResponseReader::new(Box::pin(body)); - let mut output = Vec::new(); - let mut actual = Vec::new(); - while reader.next_chunk(&mut output).await.unwrap() { - assert!(output.len() <= CHUNK_SIZE); - actual.extend_from_slice(&output); - while reader.can_read() { - reader.read_more().await; - } - assert!(reader.buffered.capacity() <= CHUNK_SIZE); - assert!(output.capacity() <= CHUNK_SIZE); - } - assert_eq!(actual, expected); - } - } - } - - #[tokio::test] - async fn flushes_partial_bytes_without_polling_pending_input() { - let body = stream::once(async { Ok(Bytes::from_static(b"data: first\n\n")) }) - .chain(stream::pending()); - let mut reader = HttpResponseReader::new(Box::pin(body)); - let mut output = Vec::new(); - assert!( - reader - .next_chunk(&mut output) - .now_or_never() - .unwrap() - .unwrap() - ); - assert_eq!(output, b"data: first\n\n"); - assert!(reader.read_more().now_or_never().is_none()); - } - - #[tokio::test] - async fn bounds_read_ahead_even_for_single_byte_fragments() { - let polls = Arc::new(AtomicUsize::new(0)); - let counter = polls.clone(); - let body = stream::repeat_with(move || { - counter.fetch_add(1, Ordering::SeqCst); - Ok(Bytes::from_static(b"x")) - }); - let mut reader = HttpResponseReader::new(Box::pin(body)); - let mut output = Vec::new(); - assert!(reader.next_chunk(&mut output).await.unwrap()); - while reader.can_read() { - reader.read_more().await; - } - assert_eq!(polls.load(Ordering::SeqCst), CHUNK_SIZE + 1); - assert_eq!(reader.buffered.len(), CHUNK_SIZE); - assert_eq!(reader.buffered.capacity(), CHUNK_SIZE); - assert_eq!(output.capacity(), CHUNK_SIZE); - assert!(reader.pending.is_empty()); - } - - struct BackingAllocation { - data: Vec, - visible: usize, - live: Arc, - } - - impl AsRef<[u8]> for BackingAllocation { - fn as_ref(&self) -> &[u8] { - &self.data[..self.visible] - } - } - - impl Drop for BackingAllocation { - fn drop(&mut self) { - self.live.fetch_sub(1, Ordering::SeqCst); - } - } - - #[tokio::test] - async fn releases_large_backing_allocations_including_empty_frames() { - let live = Arc::new(AtomicUsize::new(0)); - let counter = live.clone(); - let body = stream::iter([0, 1, 1, CHUNK_SIZE * 2]).map(move |visible| { - counter.fetch_add(1, Ordering::SeqCst); - Ok(Bytes::from_owner(BackingAllocation { - data: vec![42; CHUNK_SIZE * 64], - visible, - live: counter.clone(), - })) - }); - let mut reader = HttpResponseReader::new(Box::pin(body)); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 0); - reader.read_more().await; - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 0); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 1); - assert_eq!(reader.pending.len(), CHUNK_SIZE + 2); - let mut output = Vec::new(); - assert!(reader.next_chunk(&mut output).await.unwrap()); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 1); - assert!(reader.next_chunk(&mut output).await.unwrap()); - reader.read_more().await; - assert_eq!(live.load(Ordering::SeqCst), 0); - } - - #[tokio::test] - async fn flushes_last_partial_before_upstream_error_or_panic() { - for panic in [false, true] { - let body = stream::iter([Ok(Bytes::from_static(b"partial"))]).chain(stream::once( - async move { - assert!(!panic, "failing user stream"); - Err(CopilotRequestError::message("upstream failed")) - }, - )); - let mut reader = HttpResponseReader::new(Box::pin(body)); - reader.read_more().await; - reader.read_more().await; - let mut output = Vec::new(); - assert!(reader.next_chunk(&mut output).await.unwrap()); - assert_eq!(output, b"partial"); - let error = reader.next_chunk(&mut output).await.unwrap_err(); - assert_eq!( - error.to_string(), - if panic { - "HTTP response body stream panicked" - } else { - "upstream failed" - } - ); - } - } - - #[tokio::test] - async fn dropping_reader_drops_pending_source() { - let (tx, rx) = tokio::sync::mpsc::channel(1); - let mut reader = - HttpResponseReader::new(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx))); - assert!(reader.read_more().now_or_never().is_none()); - drop(reader); - assert!(tx.is_closed()); - } - - #[tokio::test] - async fn always_ready_empty_frames_yield_to_cancellation() { - let mut reader = - HttpResponseReader::new(Box::pin(stream::repeat_with(|| Ok(Bytes::new())))); - let mut output = Vec::new(); - assert!( - tokio::time::timeout(Duration::from_millis(10), reader.next_chunk(&mut output)) - .await - .is_err() - ); - } -} +mod tests; diff --git a/rust/src/copilot_request_handler/http_response_reader/tests.rs b/rust/src/copilot_request_handler/http_response_reader/tests.rs new file mode 100644 index 0000000000..45988726a4 --- /dev/null +++ b/rust/src/copilot_request_handler/http_response_reader/tests.rs @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use futures_util::stream; + +use super::*; + +#[tokio::test] +async fn preserves_empty_large_fragmented_and_partial_bodies() { + for size in [0, 1, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE * 3 + 7] { + let expected: Vec = (0..size).map(|i| (i % 256) as u8).collect(); + for fragment_size in [1, 1024, CHUNK_SIZE, CHUNK_SIZE * 4] { + let fragments: Vec<_> = expected + .chunks(fragment_size) + .map(Bytes::copy_from_slice) + .collect(); + let body = stream::iter( + [Bytes::new()] + .into_iter() + .chain(fragments) + .chain([Bytes::new()]) + .map(Ok), + ); + let mut reader = HttpResponseReader::new(Box::pin(body)); + let mut output = Vec::new(); + let mut actual = Vec::new(); + while reader.next_chunk(&mut output).await.unwrap() { + assert!(output.len() <= CHUNK_SIZE); + actual.extend_from_slice(&output); + while reader.can_read() { + reader.read_more().await; + } + assert!(reader.buffered.capacity() <= CHUNK_SIZE); + assert!(output.capacity() <= CHUNK_SIZE); + } + assert_eq!(actual, expected); + } + } +} + +#[tokio::test] +async fn flushes_partial_bytes_without_polling_pending_input() { + let body = + stream::once(async { Ok(Bytes::from_static(b"data: first\n\n")) }).chain(stream::pending()); + let mut reader = HttpResponseReader::new(Box::pin(body)); + let mut output = Vec::new(); + assert!( + reader + .next_chunk(&mut output) + .now_or_never() + .unwrap() + .unwrap() + ); + assert_eq!(output, b"data: first\n\n"); + assert!(reader.read_more().now_or_never().is_none()); +} + +#[tokio::test] +async fn bounds_read_ahead_even_for_single_byte_fragments() { + let polls = Arc::new(AtomicUsize::new(0)); + let counter = polls.clone(); + let body = stream::repeat_with(move || { + counter.fetch_add(1, Ordering::SeqCst); + Ok(Bytes::from_static(b"x")) + }); + let mut reader = HttpResponseReader::new(Box::pin(body)); + let mut output = Vec::new(); + assert!(reader.next_chunk(&mut output).await.unwrap()); + while reader.can_read() { + reader.read_more().await; + } + assert_eq!(polls.load(Ordering::SeqCst), CHUNK_SIZE + 1); + assert_eq!(reader.buffered.len(), CHUNK_SIZE); + assert_eq!(reader.buffered.capacity(), CHUNK_SIZE); + assert_eq!(output.capacity(), CHUNK_SIZE); + assert!(reader.pending.is_empty()); +} + +struct BackingAllocation { + data: Vec, + visible: usize, + live: Arc, +} + +impl AsRef<[u8]> for BackingAllocation { + fn as_ref(&self) -> &[u8] { + &self.data[..self.visible] + } +} + +impl Drop for BackingAllocation { + fn drop(&mut self) { + self.live.fetch_sub(1, Ordering::SeqCst); + } +} + +#[tokio::test] +async fn releases_large_backing_allocations_including_empty_frames() { + let live = Arc::new(AtomicUsize::new(0)); + let counter = live.clone(); + let body = stream::iter([0, 1, 1, CHUNK_SIZE * 2]).map(move |visible| { + counter.fetch_add(1, Ordering::SeqCst); + Ok(Bytes::from_owner(BackingAllocation { + data: vec![42; CHUNK_SIZE * 64], + visible, + live: counter.clone(), + })) + }); + let mut reader = HttpResponseReader::new(Box::pin(body)); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 0); + reader.read_more().await; + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 0); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 1); + assert_eq!(reader.pending.len(), CHUNK_SIZE + 2); + let mut output = Vec::new(); + assert!(reader.next_chunk(&mut output).await.unwrap()); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 1); + assert!(reader.next_chunk(&mut output).await.unwrap()); + reader.read_more().await; + assert_eq!(live.load(Ordering::SeqCst), 0); +} + +#[tokio::test] +async fn flushes_last_partial_before_upstream_error_or_panic() { + for panic in [false, true] { + let body = + stream::iter([Ok(Bytes::from_static(b"partial"))]).chain(stream::once(async move { + assert!(!panic, "failing user stream"); + Err(CopilotRequestError::message("upstream failed")) + })); + let mut reader = HttpResponseReader::new(Box::pin(body)); + reader.read_more().await; + reader.read_more().await; + let mut output = Vec::new(); + assert!(reader.next_chunk(&mut output).await.unwrap()); + assert_eq!(output, b"partial"); + let error = reader.next_chunk(&mut output).await.unwrap_err(); + assert_eq!( + error.to_string(), + if panic { + "HTTP response body stream panicked" + } else { + "upstream failed" + } + ); + } +} + +#[tokio::test] +async fn dropping_reader_drops_pending_source() { + let (tx, rx) = tokio::sync::mpsc::channel(1); + let mut reader = + HttpResponseReader::new(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx))); + assert!(reader.read_more().now_or_never().is_none()); + drop(reader); + assert!(tx.is_closed()); +} + +#[tokio::test] +async fn always_ready_empty_frames_yield_to_cancellation() { + let mut reader = HttpResponseReader::new(Box::pin(stream::repeat_with(|| Ok(Bytes::new())))); + let mut output = Vec::new(); + assert!( + tokio::time::timeout(Duration::from_millis(10), reader.next_chunk(&mut output)) + .await + .is_err() + ); +} diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs index a0512200c0..3baebd13f1 100644 --- a/rust/src/embeddedcli.rs +++ b/rust/src/embeddedcli.rs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + //! Lazy runtime installer for the CLI binary that build.rs embedded in this //! crate (gated on the `bundled-cli` cargo feature, which is in the default //! feature set). @@ -53,18 +55,19 @@ use tracing::{info, warn}; // supported, build.rs generates `bundled_cli.rs` exposing both selected archives. // The CLI version is exposed crate-wide via the // `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` emit (see `build.rs`), and the -// binary name is OS-derived — so no other generated constants are needed. +// release-scoped cache identity is emitted separately to avoid destination +// collisions. The binary name is OS-derived. #[cfg(has_bundled_cli)] mod build_time { include!(concat!(env!("OUT_DIR"), "/bundled_cli.rs")); } -// Pinned at build time and consumed by both install paths (path/install_at). -// Sourced from the unconditional `COPILOT_SDK_CLI_VERSION` env emit in -// build.rs — the single source of truth for "what version did build.rs -// target", shared with the runtime resolver used when `bundled-cli` is off. +// Keep the actual version for diagnostics and a release-scoped identity for +// cache paths. Legacy releases use the version unchanged for both. #[cfg(has_bundled_cli)] const CLI_VERSION: &str = env!("COPILOT_SDK_CLI_VERSION"); +#[cfg(has_bundled_cli)] +const CLI_CACHE_ID: &str = env!("COPILOT_SDK_CLI_CACHE_ID"); // OS-derived; matches the release-archive entry name and the on-disk // filename. No need to bake this — `cfg(windows)` reflects the target @@ -109,7 +112,7 @@ pub(crate) fn path() -> Option { .get_or_init(|| { #[cfg(has_bundled_cli)] { - let dir = default_install_dir(CLI_VERSION); + let dir = default_install_dir(CLI_CACHE_ID); match install_cli( &dir, build_time::CLI_ARCHIVE, @@ -171,7 +174,7 @@ pub(crate) fn runtime_path() -> Option { .get_or_init(|| { #[cfg(has_bundled_cli)] { - let dir = default_install_dir(CLI_VERSION); + let dir = default_install_dir(CLI_CACHE_ID); match install_runtime(&dir, build_time::RUNTIME_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed"); @@ -193,7 +196,7 @@ pub(crate) fn runtime_path() -> Option { pub(crate) fn install_runtime_at(extract_dir: &Path) -> Option { #[cfg(has_bundled_cli)] { - let install_dir = match runtime_install_dir(extract_dir, CLI_VERSION) { + let install_dir = match runtime_install_dir(extract_dir, CLI_CACHE_ID) { Ok(dir) => dir, Err(e) => { warn!(error = %e, "embedded runtime install directory selection failed"); @@ -1123,818 +1126,4 @@ impl std::error::Error for EmbeddedCliError { } #[cfg(test)] -mod tests { - use super::*; - - #[cfg(all(has_bundled_cli, feature = "in-process"))] - #[test] - fn embedded_runtime_archive_contains_runtime_assets_and_excludes_cli() { - let gz = flate2::read::GzDecoder::new(build_time::RUNTIME_ARCHIVE); - let mut archive = tar::Archive::new(gz); - let mut names: Vec = archive - .entries() - .expect("archive entries") - .map(|entry| { - entry - .expect("archive entry") - .path() - .expect("archive path") - .to_string_lossy() - .into_owned() - }) - .collect(); - names.sort(); - - assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string())); - assert!(names.contains(&RUNTIME_BINARY_NAME.to_string())); - assert!(names.contains(&RUNTIME_NODE_NAME.to_string())); - assert!(names.iter().any(|name| name.starts_with("ripgrep/"))); - assert!(names.iter().any(|name| name.starts_with("definitions/"))); - assert!(!names.contains(&CLI_BINARY_NAME.to_string())); - assert!(!names.contains(&"app.js".to_string())); - } - - /// Bytes whose header looks like a valid executable image on the host - /// platform, so `looks_like_valid_image` accepts them. `extra` padding - /// bytes follow the magic so size checks have something to disagree about. - fn fake_image(extra: usize) -> Vec { - let mut bytes = Vec::new(); - #[cfg(windows)] - bytes.extend_from_slice(b"MZ\x90\x00"); - #[cfg(target_os = "macos")] - bytes.extend_from_slice(&[0xfe, 0xed, 0xfa, 0xcf]); - #[cfg(all(not(windows), not(target_os = "macos")))] - bytes.extend_from_slice(b"\x7fELF"); - bytes.extend(std::iter::repeat_n(0xAB, extra)); - bytes - } - - #[test] - fn publish_verified_writes_and_records_marker() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - let bytes = fake_image(2048); - - publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); - - assert!(final_path.is_file(), "binary should be published"); - assert_eq!(fs::read(&final_path).expect("read"), bytes); - assert_eq!(read_marker_len(&marker), Some(bytes.len() as u64)); - assert!(existing_install_is_valid( - &final_path, - &marker, - bytes.len() as u64 - )); - - // No leftover temp files in the install dir. - let leftovers: Vec<_> = fs::read_dir(dir.path()) - .expect("read_dir") - .filter_map(|e| e.ok()) - .filter(|e| e.file_name().to_string_lossy().contains(".tmp.")) - .collect(); - assert!(leftovers.is_empty(), "temp files should be cleaned up"); - } - - #[test] - fn publish_overwrites_an_existing_binary() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - - // Pre-existing (stale) binary at the destination. - fs::write(&final_path, b"old contents").expect("seed"); - - let bytes = fake_image(512); - publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); - - assert_eq!(fs::read(&final_path).expect("read"), bytes); - } - - #[test] - fn corrupt_or_unmarked_install_is_rejected() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - let bytes = fake_image(4096); - - // Missing binary entirely. - assert!(!existing_install_is_valid(&final_path, &marker, 1)); - - // Valid binary but no marker (e.g. installed by an older SDK). - fs::write(&final_path, &bytes).expect("write binary"); - assert!( - !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), - "an install without a marker must not be trusted" - ); - - // Marker present but the binary was later truncated (partial write / - // antivirus). Marker still records the original full size. - write_marker(&marker, bytes.len() as u64).expect("marker"); - assert!(existing_install_is_valid( - &final_path, - &marker, - bytes.len() as u64 - )); - assert!( - !existing_install_is_valid(&final_path, &marker, bytes.len() as u64 + 1), - "a marker from the wrapper-as-CLI regression must not validate the full CLI" - ); - fs::write(&final_path, &bytes[..bytes.len() / 2]).expect("truncate"); - assert!( - !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), - "a truncated binary must be detected via the size marker" - ); - - // Zero-length binary (quarantined to empty). - fs::write(&final_path, b"").expect("empty"); - assert!(!existing_install_is_valid( - &final_path, - &marker, - bytes.len() as u64 - )); - } - - #[test] - fn invalid_image_header_is_rejected() { - let dir = tempfile::tempdir().expect("tempdir"); - let final_path = dir.path().join("copilot-bin"); - let marker = marker_path(dir.path()); - - // Right size, has a marker, but the bytes are not a valid image. - let garbage = vec![0u8; 4096]; - fs::write(&final_path, &garbage).expect("write garbage"); - write_marker(&marker, garbage.len() as u64).expect("marker"); - - assert!( - !existing_install_is_valid(&final_path, &marker, garbage.len() as u64), - "a non-executable image must be rejected even with a matching marker" - ); - } - - #[test] - fn verification_rejects_size_and_content_mismatch() { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("staged"); - let expected = fake_image(1024); - - // Exact match passes. - fs::write(&path, &expected).expect("write"); - verify_on_disk_matches(&path, &expected).expect("exact match should verify"); - - // Truncated -> size mismatch. - fs::write(&path, &expected[..100]).expect("truncate"); - assert!(verify_on_disk_matches(&path, &expected).is_err()); - - // Same length, different bytes -> content mismatch. - let mut tampered = expected.clone(); - *tampered.last_mut().expect("non-empty") ^= 0xFF; - fs::write(&path, &tampered).expect("tamper"); - assert!(verify_on_disk_matches(&path, &expected).is_err()); - - // Missing file -> I/O error. - fs::remove_file(&path).expect("remove"); - assert!(verify_on_disk_matches(&path, &expected).is_err()); - } - - #[test] - fn temp_files_are_unique_and_synced() { - let dir = tempfile::tempdir().expect("tempdir"); - let data = fake_image(256); - - let a = write_temp_file(dir.path(), &data).expect("temp a"); - let b = write_temp_file(dir.path(), &data).expect("temp b"); - - assert_ne!(a, b, "temp file names must be unique"); - assert_eq!(fs::read(&a).expect("read a"), data); - assert_eq!(fs::read(&b).expect("read b"), data); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mode = fs::metadata(&a).expect("meta").permissions().mode(); - assert_eq!(mode & 0o777, 0o755, "temp binary should be executable"); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_install_replaces_stale_pair() { - let dir = tempfile::tempdir().expect("tempdir"); - fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime"); - fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper"); - - install_runtime(dir.path(), build_time::RUNTIME_ARCHIVE).expect("install runtime"); - - assert_eq!( - fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"), - extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_NODE_NAME) - .expect("extract runtime") - ); - assert_eq!( - fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"), - extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_BINARY_NAME) - .expect("extract wrapper") - ); - } - - #[cfg(has_bundled_cli)] - #[test] - fn custom_runtime_install_dir_isolated_by_version() { - let dir = tempfile::tempdir().expect("tempdir"); - - assert_eq!( - runtime_install_dir(dir.path(), "1.0.0").expect("claim directory"), - dir.path() - ); - assert_eq!( - runtime_install_dir(dir.path(), "1.0.0").expect("reuse directory"), - dir.path() - ); - assert_eq!( - runtime_install_dir(dir.path(), "2.0.0").expect("isolate directory"), - dir.path().join("2.0.0") - ); - } - - #[cfg(has_bundled_cli)] - fn runtime_fixture(extra: &[(&str, &[u8], u32)]) -> Vec { - let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); - let mut archive = tar::Builder::new(encoder); - let mut entries = vec![ - (RUNTIME_BINARY_NAME, b"wrapper".as_slice(), 0o755), - (RUNTIME_NODE_NAME, b"runtime".as_slice(), 0o755), - ]; - #[cfg(feature = "in-process")] - entries.push((RUNTIME_LIBRARY_NAME, b"library".as_slice(), 0o644)); - entries.extend_from_slice(extra); - for (name, bytes, mode) in entries { - let mut header = tar::Header::new_gnu(); - // Raw names also let the installer see traversal fixtures which - // Builder::append_data would reject before reaching product code. - header.as_mut_bytes()[..name.len()].copy_from_slice(name.as_bytes()); - header.set_size(bytes.len() as u64); - header.set_mode(mode); - header.set_cksum(); - archive.append(&header, bytes).expect("append fixture"); - } - archive.into_inner().unwrap().finish().unwrap() - } - - #[cfg(has_bundled_cli)] - #[test] - fn warm_runtime_rejects_same_size_corruption_despite_unchanged_metadata() { - let dir = tempfile::tempdir().unwrap(); - let fixture = runtime_fixture(&[]); - install_runtime(dir.path(), &fixture).unwrap(); - let runtime = dir.path().join(RUNTIME_NODE_NAME); - let original = fs::metadata(&runtime).unwrap(); - fs::write(&runtime, b"corrupt").unwrap(); - fs::File::options() - .write(true) - .open(&runtime) - .unwrap() - .set_times(fs::FileTimes::new().set_modified(original.modified().unwrap())) - .unwrap(); - assert!(install_runtime(dir.path(), b"invalid archive").is_err()); - assert_eq!(fs::read(&runtime).unwrap(), b"corrupt"); - install_runtime(dir.path(), &fixture).unwrap(); - assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); - } - - #[cfg(has_bundled_cli)] - fn assert_no_runtime_temps(dir: &Path) { - for entry in fs::read_dir(dir).unwrap() { - let entry = entry.unwrap(); - assert!( - !entry - .file_name() - .to_string_lossy() - .starts_with(".copilot-cli.tmp.") - ); - if entry.file_type().unwrap().is_dir() { - assert_no_runtime_temps(&entry.path()); - } - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_cold_install_and_warm_reuse_preserve_bytes_and_modes() { - let dir = tempfile::tempdir().unwrap(); - let data = vec![0xAB; 3 * 64 * 1024 + 17]; - let archive = runtime_fixture(&[("nested/asset", &data, 0o640)]); - let wrapper = install_runtime(dir.path(), &archive).unwrap(); - assert_eq!(wrapper, dir.path().join(RUNTIME_BINARY_NAME)); - let asset = dir.path().join("nested/asset"); - assert_eq!(fs::read(&asset).unwrap(), data); - let modified = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1234567890); - fs::File::options() - .write(true) - .open(&asset) - .unwrap() - .set_times(fs::FileTimes::new().set_modified(modified)) - .unwrap(); - - install_runtime(dir.path(), &archive).unwrap(); - - assert_eq!(fs::metadata(&asset).unwrap().modified().unwrap(), modified); - assert_eq!(fs::read(&asset).unwrap(), data); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - assert_eq!( - fs::metadata(&asset).unwrap().permissions().mode() & 0o777, - 0o640 - ); - assert_eq!( - fs::metadata(wrapper).unwrap().permissions().mode() & 0o777, - 0o755 - ); - } - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_repairs_same_size_corruption_truncation_and_extra_bytes() { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[]); - let runtime = dir.path().join(RUNTIME_NODE_NAME); - install_runtime(dir.path(), &archive).unwrap(); - - for corrupt in [b"runtimX".as_slice(), b"run", b"", b"runtime plus garbage"] { - fs::write(&runtime, corrupt).unwrap(); - install_runtime(dir.path(), &archive).unwrap(); - assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_repairs_corruption_across_comparison_chunks() { - let dir = tempfile::tempdir().unwrap(); - let bytes = vec![0xAB; 3 * 64 * 1024 + 17]; - let archive = runtime_fixture(&[("nested/asset", &bytes, 0o644)]); - install_runtime(dir.path(), &archive).unwrap(); - for offset in [0, bytes.len() / 2, bytes.len() - 1] { - let mut corrupt = bytes.clone(); - corrupt[offset] ^= 1; - fs::write(dir.path().join("nested/asset"), &corrupt).unwrap(); - install_runtime(dir.path(), &archive).unwrap(); - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), bytes); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_reuse_preserves_executable_caller_selected_permissions() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let fixture = runtime_fixture(&[]); - let wrapper = install_runtime(dir.path(), &fixture).unwrap(); - for mode in [0o745, 0o754, 0o700, 0o500] { - fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); - install_runtime(dir.path(), &fixture).unwrap(); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - mode - ); - } - assert_no_runtime_temps(dir.path()); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_repairs_nonexecutable_wrapper() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[]); - let wrapper = install_runtime(dir.path(), &archive).unwrap(); - for mode in [0o400, 0o600] { - fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); - install_runtime(dir.path(), &archive).unwrap(); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - 0o755 - ); - assert_eq!(fs::read(&wrapper).unwrap(), b"wrapper"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_rejects_nonexecutable_wrapper_in_readonly_cache() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[]); - let wrapper = install_runtime(dir.path(), &archive).unwrap(); - fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o400)).unwrap(); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o555)).unwrap(); - let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); - let result = install_runtime(dir.path(), &archive); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); - if write_denied { - assert!( - result.is_err(), - "returned a nonexecutable wrapper: {result:?}" - ); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - 0o400 - ); - } else { - eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); - fs::remove_file(dir.path().join("write-probe")).unwrap(); - result.unwrap(); - assert_eq!( - fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, - 0o755 - ); - } - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_archive_errors_do_not_publish_and_clean_up_staging() { - let dir = tempfile::tempdir().unwrap(); - let valid = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); - let mut invalid_crc = valid.clone(); - let crc = invalid_crc.len() - 8; - invalid_crc[crc] ^= 0xFF; - let mut invalid_length = valid.clone(); - let length = invalid_length.len() - 4; - invalid_length[length] ^= 0xFF; - let mut truncated_trailer = valid.clone(); - truncated_trailer.truncate(valid.len() - 1); - let mut truncated_body = valid.clone(); - truncated_body.truncate(valid.len() / 2); - for archive in [ - invalid_crc, - invalid_length, - truncated_trailer, - truncated_body, - ] { - let runtime = dir.path().join(RUNTIME_NODE_NAME); - fs::write(&runtime, b"previous complete runtime").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(runtime).unwrap(), b"previous complete runtime"); - assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); - assert!(!dir.path().join("nested/asset").exists()); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_short_entry_is_rejected_without_publishing() { - let dir = tempfile::tempdir().unwrap(); - let mut header = tar::Header::new_gnu(); - header.set_path(RUNTIME_NODE_NAME).unwrap(); - header.set_size(128 * 1024); - header.set_mode(0o755); - header.set_cksum(); - let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); - encoder.write_all(header.as_bytes()).unwrap(); - encoder.write_all(b"short").unwrap(); - let archive = encoder.finish().unwrap(); - - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_missing_required_entry_is_rejected_before_publish() { - let dir = tempfile::tempdir().unwrap(); - let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); - let mut archive = tar::Builder::new(encoder); - let mut header = tar::Header::new_gnu(); - header.set_size(7); - header.set_mode(0o755); - header.set_cksum(); - archive - .append_data(&mut header, RUNTIME_BINARY_NAME, b"wrapper".as_slice()) - .unwrap(); - let archive = archive.into_inner().unwrap().finish().unwrap(); - - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_rejects_traversal_and_cleans_preceding_entries() { - let dir = tempfile::tempdir().unwrap(); - let install_dir = dir.path().join("install"); - let archive = runtime_fixture(&[("../escaped", b"bad", 0o644)]); - assert!(install_runtime(&install_dir, &archive).is_err()); - assert!(!dir.path().join("escaped").exists()); - assert!(!install_dir.join(RUNTIME_BINARY_NAME).exists()); - assert_no_runtime_temps(&install_dir); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_rejects_symlink_parents_and_targets() { - use std::os::unix::fs::symlink; - - let dir = tempfile::tempdir().unwrap(); - let outside = tempfile::tempdir().unwrap(); - fs::write(outside.path().join("asset"), b"outside").unwrap(); - symlink(outside.path(), dir.path().join("nested")).unwrap(); - let archive = runtime_fixture(&[("nested/asset", b"new", 0o644)]); - - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); - assert_no_runtime_temps(dir.path()); - fs::remove_file(dir.path().join("nested")).unwrap(); - symlink( - outside.path().join("asset"), - dir.path().join(RUNTIME_NODE_NAME), - ) - .unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn concurrent_runtime_installers_publish_complete_files() { - let dir = tempfile::tempdir().unwrap(); - let data = vec![0xAB; 256 * 1024 + 1]; - let archive = runtime_fixture(&[("nested/asset", &data, 0o644)]); - let barrier = std::sync::Barrier::new(6); - std::thread::scope(|scope| { - for _ in 0..6 { - scope.spawn(|| { - barrier.wait(); - install_runtime(dir.path(), &archive).unwrap(); - }); - } - }); - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), data); - assert_eq!( - fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), - b"runtime" - ); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_duplicate_destinations_fail_on_cold_and_warm_installs() { - for name in ["asset", "./asset"] { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[("asset", b"first", 0o644), (name, b"last", 0o644)]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join("asset").exists()); - assert_no_runtime_temps(dir.path()); - - fs::write(dir.path().join("asset"), b"last").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join("asset")).unwrap(), b"last"); - assert_no_runtime_temps(dir.path()); - } - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[(RUNTIME_NODE_NAME, b"", 0o755)]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); - assert_no_runtime_temps(dir.path()); - install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!( - fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), - b"runtime" - ); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_case_aliases_cannot_overwrite_required_artifacts() { - let names = [ - RUNTIME_NODE_NAME, - RUNTIME_BINARY_NAME, - #[cfg(feature = "in-process")] - RUNTIME_LIBRARY_NAME, - ]; - for name in names { - let alias = name.to_ascii_uppercase(); - let archive = runtime_fixture(&[(&alias, b"", 0o755)]); - let dir = tempfile::tempdir().unwrap(); - let result = install_runtime(dir.path(), &archive); - assert!(result.is_err(), "accepted alias {alias}: {result:?}"); - assert!(!dir.path().join(name).exists()); - assert_no_runtime_temps(dir.path()); - - install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); - let original = fs::read(dir.path().join(name)).unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join(name)).unwrap(), original); - fs::write(dir.path().join(name), b"corrupt").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join(name)).unwrap(), b"corrupt"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_mixed_separator_and_case_aliases_are_rejected() { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[ - ("nested/asset", b"first", 0o644), - (r".\NESTED\ASSET", b"last", 0o644), - ]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join("nested/asset").exists()); - assert_no_runtime_temps(dir.path()); - - install_runtime( - dir.path(), - &runtime_fixture(&[("nested/asset", b"last", 0o644)]), - ) - .unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"last"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_nonportable_alias_paths_are_rejected_before_publish() { - for name in [ - "runtime.node.", - "runtime.node ", - "runtime.node:stream", - "RUNTIM~1.NOD", - "NUL", - "con.txt", - "aux .txt", - "COM1", - "LPT9.txt", - "n\u{00e9}sted/asset", - r"..\escaped", - r"C:\escaped", - r"\\server\share\asset", - ] { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[(name, b"bad", 0o644)]); - assert!( - install_runtime(dir.path(), &archive).is_err(), - "accepted {name}" - ); - assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, feature = "in-process"))] - #[test] - fn runtime_library_aliases_are_rejected_before_repair() { - let alias = format!("./{RUNTIME_LIBRARY_NAME}"); - for duplicate in [b"another".as_slice(), b""] { - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[(&alias, duplicate, 0o644)]); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert!(!dir.path().join(RUNTIME_LIBRARY_NAME).exists()); - assert_no_runtime_temps(dir.path()); - - install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); - let library = dir.path().join(RUNTIME_LIBRARY_NAME); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(&library).unwrap(), b"library"); - fs::write(&library, b"corrupt").unwrap(); - assert!(install_runtime(dir.path(), &archive).is_err()); - assert_eq!(fs::read(&library).unwrap(), b"corrupt"); - assert_no_runtime_temps(dir.path()); - } - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn warm_runtime_install_needs_no_writable_cache() { - assert_read_only_runtime_cache(0o555, false); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn warm_runtime_reuses_owner_only_immutable_cache() { - assert_read_only_runtime_cache(0o500, false); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn warm_runtime_reuses_nonexecutable_native_library() { - assert_read_only_runtime_cache(0o555, true); - assert_read_only_runtime_cache(0o500, true); - } - - #[cfg(all(has_bundled_cli, unix))] - fn assert_read_only_runtime_cache(permission_mask: u32, readonly_native_library: bool) { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); - install_runtime(dir.path(), &archive).unwrap(); - let files = [ - RUNTIME_BINARY_NAME, - RUNTIME_NODE_NAME, - "nested/asset", - #[cfg(feature = "in-process")] - RUNTIME_LIBRARY_NAME, - ]; - let mut read_only_files = Vec::new(); - for name in files { - let path = dir.path().join(name); - let mut mode = fs::metadata(&path).unwrap().permissions().mode() & permission_mask; - if readonly_native_library && name != RUNTIME_BINARY_NAME { - mode &= !0o111; - } - fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap(); - read_only_files.push((path, mode)); - } - fs::set_permissions( - dir.path().join("nested"), - fs::Permissions::from_mode(permission_mask), - ) - .unwrap(); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(permission_mask)).unwrap(); - let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); - let result = install_runtime(dir.path(), &archive); - fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); - fs::set_permissions(dir.path().join("nested"), fs::Permissions::from_mode(0o755)).unwrap(); - result.unwrap(); - if !write_denied { - eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); - fs::remove_file(dir.path().join("write-probe")).unwrap(); - } - for (path, mode) in read_only_files { - assert_eq!( - fs::metadata(path).unwrap().permissions().mode() & 0o777, - mode - ); - } - assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"asset"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(all(has_bundled_cli, unix))] - #[test] - fn runtime_repairs_unreadable_but_replaceable_file() { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().unwrap(); - let archive = runtime_fixture(&[("asset", b"trusted", 0o000)]); - let asset = dir.path().join("asset"); - fs::write(&asset, b"corrupt").unwrap(); - fs::set_permissions(&asset, fs::Permissions::from_mode(0o000)).unwrap(); - if fs::File::open(&asset).is_ok() { - eprintln!("unreadable permission enforcement unavailable (e.g. privileged user)"); - } - let result = install_runtime(dir.path(), &archive); - let mode = fs::metadata(&asset).unwrap().permissions().mode() & 0o777; - fs::set_permissions(&asset, fs::Permissions::from_mode(0o600)).unwrap(); - result.unwrap(); - assert_eq!(mode, 0o000); - assert_eq!(fs::read(asset).unwrap(), b"trusted"); - assert_no_runtime_temps(dir.path()); - } - - #[cfg(has_bundled_cli)] - #[test] - fn runtime_replacement_preserves_open_reader_contents() { - let dir = tempfile::tempdir().unwrap(); - let asset = dir.path().join("asset"); - let original = runtime_fixture(&[("asset", b"old complete file", 0o644)]); - install_runtime(dir.path(), &original).unwrap(); - let mut reader = fs::File::open(&asset).unwrap(); - let replacement = runtime_fixture(&[("asset", b"new complete file", 0o644)]); - install_runtime(dir.path(), &replacement).unwrap(); - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).unwrap(); - assert_eq!(bytes, b"old complete file"); - assert_eq!(fs::read(&asset).unwrap(), b"new complete file"); - assert_no_runtime_temps(dir.path()); - } - - #[test] - fn failed_publish_does_not_remove_previous_file() { - let dir = tempfile::tempdir().unwrap(); - let target = dir.path().join("installed"); - fs::write(&target, b"previous complete file").unwrap(); - assert!(publish(&dir.path().join("missing-temporary"), &target).is_err()); - assert_eq!(fs::read(target).unwrap(), b"previous complete file"); - } -} +mod tests; diff --git a/rust/src/embeddedcli/tests.rs b/rust/src/embeddedcli/tests.rs new file mode 100644 index 0000000000..ab7d5281ac --- /dev/null +++ b/rust/src/embeddedcli/tests.rs @@ -0,0 +1,818 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[cfg(all(has_bundled_cli, feature = "in-process"))] +#[test] +fn embedded_runtime_archive_contains_runtime_assets_and_excludes_cli() { + let gz = flate2::read::GzDecoder::new(build_time::RUNTIME_ARCHIVE); + let mut archive = tar::Archive::new(gz); + let mut names: Vec = archive + .entries() + .expect("archive entries") + .map(|entry| { + entry + .expect("archive entry") + .path() + .expect("archive path") + .to_string_lossy() + .into_owned() + }) + .collect(); + names.sort(); + + assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_BINARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_NODE_NAME.to_string())); + assert!(names.iter().any(|name| name.starts_with("ripgrep/"))); + assert!(names.iter().any(|name| name.starts_with("definitions/"))); + assert!(!names.contains(&CLI_BINARY_NAME.to_string())); + assert!(!names.contains(&"app.js".to_string())); + assert!(!names.contains(&"cli-main.js".to_string())); +} + +/// Bytes whose header looks like a valid executable image on the host +/// platform, so `looks_like_valid_image` accepts them. `extra` padding +/// bytes follow the magic so size checks have something to disagree about. +fn fake_image(extra: usize) -> Vec { + let mut bytes = Vec::new(); + #[cfg(windows)] + bytes.extend_from_slice(b"MZ\x90\x00"); + #[cfg(target_os = "macos")] + bytes.extend_from_slice(&[0xfe, 0xed, 0xfa, 0xcf]); + #[cfg(all(not(windows), not(target_os = "macos")))] + bytes.extend_from_slice(b"\x7fELF"); + bytes.extend(std::iter::repeat_n(0xAB, extra)); + bytes +} + +#[test] +fn publish_verified_writes_and_records_marker() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + let bytes = fake_image(2048); + + publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); + + assert!(final_path.is_file(), "binary should be published"); + assert_eq!(fs::read(&final_path).expect("read"), bytes); + assert_eq!(read_marker_len(&marker), Some(bytes.len() as u64)); + assert!(existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); + + // No leftover temp files in the install dir. + let leftovers: Vec<_> = fs::read_dir(dir.path()) + .expect("read_dir") + .filter_map(|e| e.ok()) + .filter(|e| e.file_name().to_string_lossy().contains(".tmp.")) + .collect(); + assert!(leftovers.is_empty(), "temp files should be cleaned up"); +} + +#[test] +fn publish_overwrites_an_existing_binary() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + + // Pre-existing (stale) binary at the destination. + fs::write(&final_path, b"old contents").expect("seed"); + + let bytes = fake_image(512); + publish_verified(dir.path(), &final_path, &marker, &bytes).expect("publish"); + + assert_eq!(fs::read(&final_path).expect("read"), bytes); +} + +#[test] +fn corrupt_or_unmarked_install_is_rejected() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + let bytes = fake_image(4096); + + // Missing binary entirely. + assert!(!existing_install_is_valid(&final_path, &marker, 1)); + + // Valid binary but no marker (e.g. installed by an older SDK). + fs::write(&final_path, &bytes).expect("write binary"); + assert!( + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), + "an install without a marker must not be trusted" + ); + + // Marker present but the binary was later truncated (partial write / + // antivirus). Marker still records the original full size. + write_marker(&marker, bytes.len() as u64).expect("marker"); + assert!(existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); + assert!( + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64 + 1), + "a marker from the wrapper-as-CLI regression must not validate the full CLI" + ); + fs::write(&final_path, &bytes[..bytes.len() / 2]).expect("truncate"); + assert!( + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), + "a truncated binary must be detected via the size marker" + ); + + // Zero-length binary (quarantined to empty). + fs::write(&final_path, b"").expect("empty"); + assert!(!existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); +} + +#[test] +fn invalid_image_header_is_rejected() { + let dir = tempfile::tempdir().expect("tempdir"); + let final_path = dir.path().join("copilot-bin"); + let marker = marker_path(dir.path()); + + // Right size, has a marker, but the bytes are not a valid image. + let garbage = vec![0u8; 4096]; + fs::write(&final_path, &garbage).expect("write garbage"); + write_marker(&marker, garbage.len() as u64).expect("marker"); + + assert!( + !existing_install_is_valid(&final_path, &marker, garbage.len() as u64), + "a non-executable image must be rejected even with a matching marker" + ); +} + +#[test] +fn verification_rejects_size_and_content_mismatch() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("staged"); + let expected = fake_image(1024); + + // Exact match passes. + fs::write(&path, &expected).expect("write"); + verify_on_disk_matches(&path, &expected).expect("exact match should verify"); + + // Truncated -> size mismatch. + fs::write(&path, &expected[..100]).expect("truncate"); + assert!(verify_on_disk_matches(&path, &expected).is_err()); + + // Same length, different bytes -> content mismatch. + let mut tampered = expected.clone(); + *tampered.last_mut().expect("non-empty") ^= 0xFF; + fs::write(&path, &tampered).expect("tamper"); + assert!(verify_on_disk_matches(&path, &expected).is_err()); + + // Missing file -> I/O error. + fs::remove_file(&path).expect("remove"); + assert!(verify_on_disk_matches(&path, &expected).is_err()); +} + +#[test] +fn temp_files_are_unique_and_synced() { + let dir = tempfile::tempdir().expect("tempdir"); + let data = fake_image(256); + + let a = write_temp_file(dir.path(), &data).expect("temp a"); + let b = write_temp_file(dir.path(), &data).expect("temp b"); + + assert_ne!(a, b, "temp file names must be unique"); + assert_eq!(fs::read(&a).expect("read a"), data); + assert_eq!(fs::read(&b).expect("read b"), data); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = fs::metadata(&a).expect("meta").permissions().mode(); + assert_eq!(mode & 0o777, 0o755, "temp binary should be executable"); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_install_replaces_stale_pair() { + let dir = tempfile::tempdir().expect("tempdir"); + fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime"); + fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper"); + + install_runtime(dir.path(), build_time::RUNTIME_ARCHIVE).expect("install runtime"); + + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"), + extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_NODE_NAME).expect("extract runtime") + ); + assert_eq!( + fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"), + extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_BINARY_NAME).expect("extract wrapper") + ); +} + +#[cfg(has_bundled_cli)] +#[test] +fn custom_runtime_install_dir_isolated_by_version() { + let dir = tempfile::tempdir().expect("tempdir"); + + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("claim directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("reuse directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "2.0.0").expect("isolate directory"), + dir.path().join("2.0.0") + ); +} + +#[cfg(has_bundled_cli)] +fn runtime_fixture(extra: &[(&str, &[u8], u32)]) -> Vec { + let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + let mut archive = tar::Builder::new(encoder); + let mut entries = vec![ + (RUNTIME_BINARY_NAME, b"wrapper".as_slice(), 0o755), + (RUNTIME_NODE_NAME, b"runtime".as_slice(), 0o755), + ]; + #[cfg(feature = "in-process")] + entries.push((RUNTIME_LIBRARY_NAME, b"library".as_slice(), 0o644)); + entries.extend_from_slice(extra); + for (name, bytes, mode) in entries { + let mut header = tar::Header::new_gnu(); + // Raw names also let the installer see traversal fixtures which + // Builder::append_data would reject before reaching product code. + header.as_mut_bytes()[..name.len()].copy_from_slice(name.as_bytes()); + header.set_size(bytes.len() as u64); + header.set_mode(mode); + header.set_cksum(); + archive.append(&header, bytes).expect("append fixture"); + } + archive.into_inner().unwrap().finish().unwrap() +} + +#[cfg(has_bundled_cli)] +#[test] +fn warm_runtime_rejects_same_size_corruption_despite_unchanged_metadata() { + let dir = tempfile::tempdir().unwrap(); + let fixture = runtime_fixture(&[]); + install_runtime(dir.path(), &fixture).unwrap(); + let runtime = dir.path().join(RUNTIME_NODE_NAME); + let original = fs::metadata(&runtime).unwrap(); + fs::write(&runtime, b"corrupt").unwrap(); + fs::File::options() + .write(true) + .open(&runtime) + .unwrap() + .set_times(fs::FileTimes::new().set_modified(original.modified().unwrap())) + .unwrap(); + assert!(install_runtime(dir.path(), b"invalid archive").is_err()); + assert_eq!(fs::read(&runtime).unwrap(), b"corrupt"); + install_runtime(dir.path(), &fixture).unwrap(); + assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); +} + +#[cfg(has_bundled_cli)] +fn assert_no_runtime_temps(dir: &Path) { + for entry in fs::read_dir(dir).unwrap() { + let entry = entry.unwrap(); + assert!( + !entry + .file_name() + .to_string_lossy() + .starts_with(".copilot-cli.tmp.") + ); + if entry.file_type().unwrap().is_dir() { + assert_no_runtime_temps(&entry.path()); + } + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_cold_install_and_warm_reuse_preserve_bytes_and_modes() { + let dir = tempfile::tempdir().unwrap(); + let data = vec![0xAB; 3 * 64 * 1024 + 17]; + let archive = runtime_fixture(&[("nested/asset", &data, 0o640)]); + let wrapper = install_runtime(dir.path(), &archive).unwrap(); + assert_eq!(wrapper, dir.path().join(RUNTIME_BINARY_NAME)); + let asset = dir.path().join("nested/asset"); + assert_eq!(fs::read(&asset).unwrap(), data); + let modified = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1234567890); + fs::File::options() + .write(true) + .open(&asset) + .unwrap() + .set_times(fs::FileTimes::new().set_modified(modified)) + .unwrap(); + + install_runtime(dir.path(), &archive).unwrap(); + + assert_eq!(fs::metadata(&asset).unwrap().modified().unwrap(), modified); + assert_eq!(fs::read(&asset).unwrap(), data); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + fs::metadata(&asset).unwrap().permissions().mode() & 0o777, + 0o640 + ); + assert_eq!( + fs::metadata(wrapper).unwrap().permissions().mode() & 0o777, + 0o755 + ); + } + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_repairs_same_size_corruption_truncation_and_extra_bytes() { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[]); + let runtime = dir.path().join(RUNTIME_NODE_NAME); + install_runtime(dir.path(), &archive).unwrap(); + + for corrupt in [b"runtimX".as_slice(), b"run", b"", b"runtime plus garbage"] { + fs::write(&runtime, corrupt).unwrap(); + install_runtime(dir.path(), &archive).unwrap(); + assert_eq!(fs::read(&runtime).unwrap(), b"runtime"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_repairs_corruption_across_comparison_chunks() { + let dir = tempfile::tempdir().unwrap(); + let bytes = vec![0xAB; 3 * 64 * 1024 + 17]; + let archive = runtime_fixture(&[("nested/asset", &bytes, 0o644)]); + install_runtime(dir.path(), &archive).unwrap(); + for offset in [0, bytes.len() / 2, bytes.len() - 1] { + let mut corrupt = bytes.clone(); + corrupt[offset] ^= 1; + fs::write(dir.path().join("nested/asset"), &corrupt).unwrap(); + install_runtime(dir.path(), &archive).unwrap(); + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), bytes); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_reuse_preserves_executable_caller_selected_permissions() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let fixture = runtime_fixture(&[]); + let wrapper = install_runtime(dir.path(), &fixture).unwrap(); + for mode in [0o745, 0o754, 0o700, 0o500] { + fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); + install_runtime(dir.path(), &fixture).unwrap(); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + mode + ); + } + assert_no_runtime_temps(dir.path()); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_repairs_nonexecutable_wrapper() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[]); + let wrapper = install_runtime(dir.path(), &archive).unwrap(); + for mode in [0o400, 0o600] { + fs::set_permissions(&wrapper, fs::Permissions::from_mode(mode)).unwrap(); + install_runtime(dir.path(), &archive).unwrap(); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + 0o755 + ); + assert_eq!(fs::read(&wrapper).unwrap(), b"wrapper"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_rejects_nonexecutable_wrapper_in_readonly_cache() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[]); + let wrapper = install_runtime(dir.path(), &archive).unwrap(); + fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o400)).unwrap(); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o555)).unwrap(); + let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); + let result = install_runtime(dir.path(), &archive); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); + if write_denied { + assert!( + result.is_err(), + "returned a nonexecutable wrapper: {result:?}" + ); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + 0o400 + ); + } else { + eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); + fs::remove_file(dir.path().join("write-probe")).unwrap(); + result.unwrap(); + assert_eq!( + fs::metadata(&wrapper).unwrap().permissions().mode() & 0o777, + 0o755 + ); + } + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_archive_errors_do_not_publish_and_clean_up_staging() { + let dir = tempfile::tempdir().unwrap(); + let valid = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); + let mut invalid_crc = valid.clone(); + let crc = invalid_crc.len() - 8; + invalid_crc[crc] ^= 0xFF; + let mut invalid_length = valid.clone(); + let length = invalid_length.len() - 4; + invalid_length[length] ^= 0xFF; + let mut truncated_trailer = valid.clone(); + truncated_trailer.truncate(valid.len() - 1); + let mut truncated_body = valid.clone(); + truncated_body.truncate(valid.len() / 2); + for archive in [ + invalid_crc, + invalid_length, + truncated_trailer, + truncated_body, + ] { + let runtime = dir.path().join(RUNTIME_NODE_NAME); + fs::write(&runtime, b"previous complete runtime").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(runtime).unwrap(), b"previous complete runtime"); + assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); + assert!(!dir.path().join("nested/asset").exists()); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_short_entry_is_rejected_without_publishing() { + let dir = tempfile::tempdir().unwrap(); + let mut header = tar::Header::new_gnu(); + header.set_path(RUNTIME_NODE_NAME).unwrap(); + header.set_size(128 * 1024); + header.set_mode(0o755); + header.set_cksum(); + let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + encoder.write_all(header.as_bytes()).unwrap(); + encoder.write_all(b"short").unwrap(); + let archive = encoder.finish().unwrap(); + + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_missing_required_entry_is_rejected_before_publish() { + let dir = tempfile::tempdir().unwrap(); + let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + let mut archive = tar::Builder::new(encoder); + let mut header = tar::Header::new_gnu(); + header.set_size(7); + header.set_mode(0o755); + header.set_cksum(); + archive + .append_data(&mut header, RUNTIME_BINARY_NAME, b"wrapper".as_slice()) + .unwrap(); + let archive = archive.into_inner().unwrap().finish().unwrap(); + + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_BINARY_NAME).exists()); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_rejects_traversal_and_cleans_preceding_entries() { + let dir = tempfile::tempdir().unwrap(); + let install_dir = dir.path().join("install"); + let archive = runtime_fixture(&[("../escaped", b"bad", 0o644)]); + assert!(install_runtime(&install_dir, &archive).is_err()); + assert!(!dir.path().join("escaped").exists()); + assert!(!install_dir.join(RUNTIME_BINARY_NAME).exists()); + assert_no_runtime_temps(&install_dir); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_rejects_symlink_parents_and_targets() { + use std::os::unix::fs::symlink; + + let dir = tempfile::tempdir().unwrap(); + let outside = tempfile::tempdir().unwrap(); + fs::write(outside.path().join("asset"), b"outside").unwrap(); + symlink(outside.path(), dir.path().join("nested")).unwrap(); + let archive = runtime_fixture(&[("nested/asset", b"new", 0o644)]); + + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); + assert_no_runtime_temps(dir.path()); + fs::remove_file(dir.path().join("nested")).unwrap(); + symlink( + outside.path().join("asset"), + dir.path().join(RUNTIME_NODE_NAME), + ) + .unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(outside.path().join("asset")).unwrap(), b"outside"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn concurrent_runtime_installers_publish_complete_files() { + let dir = tempfile::tempdir().unwrap(); + let data = vec![0xAB; 256 * 1024 + 1]; + let archive = runtime_fixture(&[("nested/asset", &data, 0o644)]); + let barrier = std::sync::Barrier::new(6); + std::thread::scope(|scope| { + for _ in 0..6 { + scope.spawn(|| { + barrier.wait(); + install_runtime(dir.path(), &archive).unwrap(); + }); + } + }); + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), data); + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), + b"runtime" + ); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_duplicate_destinations_fail_on_cold_and_warm_installs() { + for name in ["asset", "./asset"] { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[("asset", b"first", 0o644), (name, b"last", 0o644)]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join("asset").exists()); + assert_no_runtime_temps(dir.path()); + + fs::write(dir.path().join("asset"), b"last").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join("asset")).unwrap(), b"last"); + assert_no_runtime_temps(dir.path()); + } + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[(RUNTIME_NODE_NAME, b"", 0o755)]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); + assert_no_runtime_temps(dir.path()); + install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).unwrap(), + b"runtime" + ); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_case_aliases_cannot_overwrite_required_artifacts() { + let names = [ + RUNTIME_NODE_NAME, + RUNTIME_BINARY_NAME, + #[cfg(feature = "in-process")] + RUNTIME_LIBRARY_NAME, + ]; + for name in names { + let alias = name.to_ascii_uppercase(); + let archive = runtime_fixture(&[(&alias, b"", 0o755)]); + let dir = tempfile::tempdir().unwrap(); + let result = install_runtime(dir.path(), &archive); + assert!(result.is_err(), "accepted alias {alias}: {result:?}"); + assert!(!dir.path().join(name).exists()); + assert_no_runtime_temps(dir.path()); + + install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); + let original = fs::read(dir.path().join(name)).unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join(name)).unwrap(), original); + fs::write(dir.path().join(name), b"corrupt").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join(name)).unwrap(), b"corrupt"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_mixed_separator_and_case_aliases_are_rejected() { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[ + ("nested/asset", b"first", 0o644), + (r".\NESTED\ASSET", b"last", 0o644), + ]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join("nested/asset").exists()); + assert_no_runtime_temps(dir.path()); + + install_runtime( + dir.path(), + &runtime_fixture(&[("nested/asset", b"last", 0o644)]), + ) + .unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"last"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_nonportable_alias_paths_are_rejected_before_publish() { + for name in [ + "runtime.node.", + "runtime.node ", + "runtime.node:stream", + "RUNTIM~1.NOD", + "NUL", + "con.txt", + "aux .txt", + "COM1", + "LPT9.txt", + "n\u{00e9}sted/asset", + r"..\escaped", + r"C:\escaped", + r"\\server\share\asset", + ] { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[(name, b"bad", 0o644)]); + assert!( + install_runtime(dir.path(), &archive).is_err(), + "accepted {name}" + ); + assert!(!dir.path().join(RUNTIME_NODE_NAME).exists()); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, feature = "in-process"))] +#[test] +fn runtime_library_aliases_are_rejected_before_repair() { + let alias = format!("./{RUNTIME_LIBRARY_NAME}"); + for duplicate in [b"another".as_slice(), b""] { + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[(&alias, duplicate, 0o644)]); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert!(!dir.path().join(RUNTIME_LIBRARY_NAME).exists()); + assert_no_runtime_temps(dir.path()); + + install_runtime(dir.path(), &runtime_fixture(&[])).unwrap(); + let library = dir.path().join(RUNTIME_LIBRARY_NAME); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(&library).unwrap(), b"library"); + fs::write(&library, b"corrupt").unwrap(); + assert!(install_runtime(dir.path(), &archive).is_err()); + assert_eq!(fs::read(&library).unwrap(), b"corrupt"); + assert_no_runtime_temps(dir.path()); + } +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn warm_runtime_install_needs_no_writable_cache() { + assert_read_only_runtime_cache(0o555, false); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn warm_runtime_reuses_owner_only_immutable_cache() { + assert_read_only_runtime_cache(0o500, false); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn warm_runtime_reuses_nonexecutable_native_library() { + assert_read_only_runtime_cache(0o555, true); + assert_read_only_runtime_cache(0o500, true); +} + +#[cfg(all(has_bundled_cli, unix))] +fn assert_read_only_runtime_cache(permission_mask: u32, readonly_native_library: bool) { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[("nested/asset", b"asset", 0o644)]); + install_runtime(dir.path(), &archive).unwrap(); + let files = [ + RUNTIME_BINARY_NAME, + RUNTIME_NODE_NAME, + "nested/asset", + #[cfg(feature = "in-process")] + RUNTIME_LIBRARY_NAME, + ]; + let mut read_only_files = Vec::new(); + for name in files { + let path = dir.path().join(name); + let mut mode = fs::metadata(&path).unwrap().permissions().mode() & permission_mask; + if readonly_native_library && name != RUNTIME_BINARY_NAME { + mode &= !0o111; + } + fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap(); + read_only_files.push((path, mode)); + } + fs::set_permissions( + dir.path().join("nested"), + fs::Permissions::from_mode(permission_mask), + ) + .unwrap(); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(permission_mask)).unwrap(); + let write_denied = fs::File::create(dir.path().join("write-probe")).is_err(); + let result = install_runtime(dir.path(), &archive); + fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap(); + fs::set_permissions(dir.path().join("nested"), fs::Permissions::from_mode(0o755)).unwrap(); + result.unwrap(); + if !write_denied { + eprintln!("read-only permission enforcement unavailable (e.g. privileged user)"); + fs::remove_file(dir.path().join("write-probe")).unwrap(); + } + for (path, mode) in read_only_files { + assert_eq!( + fs::metadata(path).unwrap().permissions().mode() & 0o777, + mode + ); + } + assert_eq!(fs::read(dir.path().join("nested/asset")).unwrap(), b"asset"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(all(has_bundled_cli, unix))] +#[test] +fn runtime_repairs_unreadable_but_replaceable_file() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let archive = runtime_fixture(&[("asset", b"trusted", 0o000)]); + let asset = dir.path().join("asset"); + fs::write(&asset, b"corrupt").unwrap(); + fs::set_permissions(&asset, fs::Permissions::from_mode(0o000)).unwrap(); + if fs::File::open(&asset).is_ok() { + eprintln!("unreadable permission enforcement unavailable (e.g. privileged user)"); + } + let result = install_runtime(dir.path(), &archive); + let mode = fs::metadata(&asset).unwrap().permissions().mode() & 0o777; + fs::set_permissions(&asset, fs::Permissions::from_mode(0o600)).unwrap(); + result.unwrap(); + assert_eq!(mode, 0o000); + assert_eq!(fs::read(asset).unwrap(), b"trusted"); + assert_no_runtime_temps(dir.path()); +} + +#[cfg(has_bundled_cli)] +#[test] +fn runtime_replacement_preserves_open_reader_contents() { + let dir = tempfile::tempdir().unwrap(); + let asset = dir.path().join("asset"); + let original = runtime_fixture(&[("asset", b"old complete file", 0o644)]); + install_runtime(dir.path(), &original).unwrap(); + let mut reader = fs::File::open(&asset).unwrap(); + let replacement = runtime_fixture(&[("asset", b"new complete file", 0o644)]); + install_runtime(dir.path(), &replacement).unwrap(); + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).unwrap(); + assert_eq!(bytes, b"old complete file"); + assert_eq!(fs::read(&asset).unwrap(), b"new complete file"); + assert_no_runtime_temps(dir.path()); +} + +#[test] +fn failed_publish_does_not_remove_previous_file() { + let dir = tempfile::tempdir().unwrap(); + let target = dir.path().join("installed"); + fs::write(&target, b"previous complete file").unwrap(); + assert!(publish(&dir.path().join("missing-temporary"), &target).is_err()); + assert_eq!(fs::read(target).unwrap(), b"previous complete file"); +} diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index bbd4138099..df0269b0b8 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -594,154 +594,7 @@ fn build_env_json(environment: &[(String, String)]) -> Option> { } #[cfg(test)] -mod tests { - use std::sync::Mutex; - use std::sync::atomic::AtomicUsize; - use std::time::{Duration, Instant}; - - use super::*; - - static FFI_LIFECYCLE_TEST_LOCK: Mutex<()> = Mutex::new(()); - static TEST_ALLOW_CLOSE: AtomicBool = AtomicBool::new(false); - static TEST_CLOSE_CALLS: AtomicUsize = AtomicUsize::new(0); - static TEST_SHUTDOWN_CALLS: AtomicUsize = AtomicUsize::new(0); - - unsafe extern "C" fn test_host_shutdown(_server_id: u32) -> bool { - TEST_SHUTDOWN_CALLS.fetch_add(1, Ordering::SeqCst); - true - } - - unsafe extern "C" fn test_connection_write( - _connection_id: u32, - _bytes: *const u8, - _length: usize, - ) -> bool { - true - } - - unsafe extern "C" fn test_connection_close(_connection_id: u32) -> bool { - TEST_CLOSE_CALLS.fetch_add(1, Ordering::SeqCst); - TEST_ALLOW_CLOSE.load(Ordering::SeqCst) - } - - #[test] - fn argv_without_entrypoint_contains_only_client_options() { - let argv: Vec = serde_json::from_slice(&build_argv_json( - None, - &["--log-level".into(), "debug".into()], - )) - .unwrap(); - - assert_eq!(argv, ["--log-level", "debug"]); - } - - #[test] - fn explicit_javascript_entrypoint_uses_node() { - let argv: Vec = - serde_json::from_slice(&build_argv_json(Some(Path::new("index.js")), &[])).unwrap(); - - assert_eq!( - argv, - ["node", "index.js", "--embedded-host", "--no-auto-update"] - ); - } - - #[cfg(windows)] - #[test] - fn child_process_path_removes_windows_verbatim_prefix() { - assert_eq!( - path_for_child_process(PathBuf::from(r"\\?\D:\a\copilot-sdk\index.js")), - PathBuf::from(r"D:\a\copilot-sdk\index.js") - ); - assert_eq!( - path_for_child_process(PathBuf::from(r"\\?\UNC\server\share\copilot-sdk\index.js")), - PathBuf::from(r"\\server\share\copilot-sdk\index.js") - ); - } - - #[test] - fn environment_is_omitted_when_empty() { - assert_eq!(build_env_json(&[]), None); - } - - #[test] - fn environment_serializes_worker_overrides() { - let env: serde_json::Value = serde_json::from_slice( - &build_env_json(&[ - ("COPILOT_HOME".into(), "state".into()), - ("COPILOT_DISABLE_KEYTAR".into(), "1".into()), - ]) - .unwrap(), - ) - .unwrap(); - - assert_eq!( - env, - serde_json::json!({ - "COPILOT_HOME": "state", - "COPILOT_DISABLE_KEYTAR": "1", - }) - ); - } - - #[test] - fn callback_state_is_retained_until_connection_close_succeeds() { - let _guard = FFI_LIFECYCLE_TEST_LOCK.lock().unwrap(); - TEST_ALLOW_CLOSE.store(false, Ordering::SeqCst); - TEST_CLOSE_CALLS.store(0, Ordering::SeqCst); - TEST_SHUTDOWN_CALLS.store(0, Ordering::SeqCst); - - let (tx, mut rx) = mpsc::unbounded_channel(); - let state_ptr = Box::into_raw(Box::new(CallbackState { - tx, - closing: AtomicBool::new(false), - })); - let shared = FfiShared { - host_shutdown: test_host_shutdown, - connection_write: test_connection_write, - connection_close: test_connection_close, - server_id: AtomicU32::new(11), - connection_id: AtomicU32::new(21), - callback_state: AtomicPtr::new(state_ptr), - closed: AtomicBool::new(false), - operation_lock: parking_lot::Mutex::new(()), - library_path: PathBuf::from("test-runtime"), - }; - - shared.close(); - - assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 1); - assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 0); - assert_eq!(shared.connection_id.load(Ordering::SeqCst), 0); - assert!(shared.callback_state.load(Ordering::SeqCst).is_null()); - - assert!(matches!( - rx.try_recv(), - Err(mpsc::error::TryRecvError::Empty) - )); - - TEST_ALLOW_CLOSE.store(true, Ordering::SeqCst); - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline && TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst) == 0 { - std::thread::sleep(Duration::from_millis(10)); - } - - assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 2); - assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); - assert!(matches!( - rx.try_recv(), - Err(mpsc::error::TryRecvError::Disconnected) - )); - - let close_calls_after_cleanup = TEST_CLOSE_CALLS.load(Ordering::SeqCst); - shared.close(); - assert_eq!( - TEST_CLOSE_CALLS.load(Ordering::SeqCst), - close_calls_after_cleanup - ); - assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); - } -} +mod tests; #[cfg(test)] mod shutdown_tests; diff --git a/rust/src/ffi/tests.rs b/rust/src/ffi/tests.rs new file mode 100644 index 0000000000..5662dc4e5b --- /dev/null +++ b/rust/src/ffi/tests.rs @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::Mutex; +use std::sync::atomic::AtomicUsize; +use std::time::{Duration, Instant}; + +use super::*; + +static FFI_LIFECYCLE_TEST_LOCK: Mutex<()> = Mutex::new(()); +static TEST_ALLOW_CLOSE: AtomicBool = AtomicBool::new(false); +static TEST_CLOSE_CALLS: AtomicUsize = AtomicUsize::new(0); +static TEST_SHUTDOWN_CALLS: AtomicUsize = AtomicUsize::new(0); + +unsafe extern "C" fn test_host_shutdown(_server_id: u32) -> bool { + TEST_SHUTDOWN_CALLS.fetch_add(1, Ordering::SeqCst); + true +} + +unsafe extern "C" fn test_connection_write( + _connection_id: u32, + _bytes: *const u8, + _length: usize, +) -> bool { + true +} + +unsafe extern "C" fn test_connection_close(_connection_id: u32) -> bool { + TEST_CLOSE_CALLS.fetch_add(1, Ordering::SeqCst); + TEST_ALLOW_CLOSE.load(Ordering::SeqCst) +} + +#[test] +fn argv_without_entrypoint_contains_only_client_options() { + let argv: Vec = serde_json::from_slice(&build_argv_json( + None, + &["--log-level".into(), "debug".into()], + )) + .unwrap(); + + assert_eq!(argv, ["--log-level", "debug"]); +} + +#[test] +fn explicit_javascript_entrypoint_uses_node() { + let argv: Vec = + serde_json::from_slice(&build_argv_json(Some(Path::new("index.js")), &[])).unwrap(); + + assert_eq!( + argv, + ["node", "index.js", "--embedded-host", "--no-auto-update"] + ); +} + +#[cfg(windows)] +#[test] +fn child_process_path_removes_windows_verbatim_prefix() { + assert_eq!( + path_for_child_process(PathBuf::from(r"\\?\D:\a\copilot-sdk\index.js")), + PathBuf::from(r"D:\a\copilot-sdk\index.js") + ); + assert_eq!( + path_for_child_process(PathBuf::from(r"\\?\UNC\server\share\copilot-sdk\index.js")), + PathBuf::from(r"\\server\share\copilot-sdk\index.js") + ); +} + +#[test] +fn environment_is_omitted_when_empty() { + assert_eq!(build_env_json(&[]), None); +} + +#[test] +fn environment_serializes_worker_overrides() { + let env: serde_json::Value = serde_json::from_slice( + &build_env_json(&[ + ("COPILOT_HOME".into(), "state".into()), + ("COPILOT_DISABLE_KEYTAR".into(), "1".into()), + ]) + .unwrap(), + ) + .unwrap(); + + assert_eq!( + env, + serde_json::json!({ + "COPILOT_HOME": "state", + "COPILOT_DISABLE_KEYTAR": "1", + }) + ); +} + +#[test] +fn callback_state_is_retained_until_connection_close_succeeds() { + let _guard = FFI_LIFECYCLE_TEST_LOCK.lock().unwrap(); + TEST_ALLOW_CLOSE.store(false, Ordering::SeqCst); + TEST_CLOSE_CALLS.store(0, Ordering::SeqCst); + TEST_SHUTDOWN_CALLS.store(0, Ordering::SeqCst); + + let (tx, mut rx) = mpsc::unbounded_channel(); + let state_ptr = Box::into_raw(Box::new(CallbackState { + tx, + closing: AtomicBool::new(false), + })); + let shared = FfiShared { + host_shutdown: test_host_shutdown, + connection_write: test_connection_write, + connection_close: test_connection_close, + server_id: AtomicU32::new(11), + connection_id: AtomicU32::new(21), + callback_state: AtomicPtr::new(state_ptr), + closed: AtomicBool::new(false), + operation_lock: parking_lot::Mutex::new(()), + library_path: PathBuf::from("test-runtime"), + }; + + shared.close(); + + assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 1); + assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 0); + assert_eq!(shared.connection_id.load(Ordering::SeqCst), 0); + assert!(shared.callback_state.load(Ordering::SeqCst).is_null()); + + assert!(matches!( + rx.try_recv(), + Err(mpsc::error::TryRecvError::Empty) + )); + + TEST_ALLOW_CLOSE.store(true, Ordering::SeqCst); + let deadline = Instant::now() + Duration::from_secs(5); + while Instant::now() < deadline && TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst) == 0 { + std::thread::sleep(Duration::from_millis(10)); + } + + assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 2); + assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); + assert!(matches!( + rx.try_recv(), + Err(mpsc::error::TryRecvError::Disconnected) + )); + + let close_calls_after_cleanup = TEST_CLOSE_CALLS.load(Ordering::SeqCst); + shared.close(); + assert_eq!( + TEST_CLOSE_CALLS.load(Ordering::SeqCst), + close_calls_after_cleanup + ); + assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1); +} diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index b73e187d2d..8069bbf04a 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -56,6 +56,16 @@ pub mod rpc_methods { pub const MODELS_GETBUILTINCATALOG: &str = "models.getBuiltInCatalog"; /// `sandbox.getHostSupport` pub const SANDBOX_GETHOSTSUPPORT: &str = "sandbox.getHostSupport"; + /// `sandbox.proxyCa.getStatus` + pub const SANDBOX_PROXYCA_GETSTATUS: &str = "sandbox.proxyCa.getStatus"; + /// `sandbox.proxyCa.create` + pub const SANDBOX_PROXYCA_CREATE: &str = "sandbox.proxyCa.create"; + /// `sandbox.proxyCa.rotate` + pub const SANDBOX_PROXYCA_ROTATE: &str = "sandbox.proxyCa.rotate"; + /// `sandbox.proxyCa.trust` + pub const SANDBOX_PROXYCA_TRUST: &str = "sandbox.proxyCa.trust"; + /// `sandbox.proxyCa.remove` + pub const SANDBOX_PROXYCA_REMOVE: &str = "sandbox.proxyCa.remove"; /// `tools.list` pub const TOOLS_LIST: &str = "tools.list"; /// `account.getQuota` @@ -172,18 +182,47 @@ pub mod rpc_methods { pub const AGENTS_DISCOVER: &str = "agents.discover"; /// `agents.getDiscoveryPaths` pub const AGENTS_GETDISCOVERYPATHS: &str = "agents.getDiscoveryPaths"; + /// `agents.getBuiltins` + pub const AGENTS_GETBUILTINS: &str = "agents.getBuiltins"; + /// `agents.getAvailableBuiltins` + pub const AGENTS_GETAVAILABLEBUILTINS: &str = "agents.getAvailableBuiltins"; + /// `agents.getBuiltinDefinition` + pub const AGENTS_GETBUILTINDEFINITION: &str = "agents.getBuiltinDefinition"; + /// `agents.getBuiltinListingDefinition` + pub const AGENTS_GETBUILTINLISTINGDEFINITION: &str = "agents.getBuiltinListingDefinition"; + /// `agents.customAgentInitialModelDecision` + pub const AGENTS_CUSTOMAGENTINITIALMODELDECISION: &str = + "agents.customAgentInitialModelDecision"; /// `instructions.discover` pub const INSTRUCTIONS_DISCOVER: &str = "instructions.discover"; /// `instructions.getDiscoveryPaths` pub const INSTRUCTIONS_GETDISCOVERYPATHS: &str = "instructions.getDiscoveryPaths"; + /// `globalState.load` + pub const GLOBALSTATE_LOAD: &str = "globalState.load"; + /// `globalState.loadForConfigDir` + pub const GLOBALSTATE_LOADFORCONFIGDIR: &str = "globalState.loadForConfigDir"; + /// `globalState.writeKey` + pub const GLOBALSTATE_WRITEKEY: &str = "globalState.writeKey"; /// `commands.list` pub const COMMANDS_LIST: &str = "commands.list"; - /// `user.settings.reload` - pub const USER_SETTINGS_RELOAD: &str = "user.settings.reload"; /// `user.settings.get` pub const USER_SETTINGS_GET: &str = "user.settings.get"; /// `user.settings.set` pub const USER_SETTINGS_SET: &str = "user.settings.set"; + /// `gitHubRepository.atPath` + pub const GITHUBREPOSITORY_ATPATH: &str = "gitHubRepository.atPath"; + /// `gitHubOwners.nextRequestId` + pub const GITHUBOWNERS_NEXTREQUESTID: &str = "gitHubOwners.nextRequestId"; + /// `gitHubOwners.list` + pub const GITHUBOWNERS_LIST: &str = "gitHubOwners.list"; + /// `gitHubOwners.cancel` + pub const GITHUBOWNERS_CANCEL: &str = "gitHubOwners.cancel"; + /// `git.currentBranchRemote` + pub const GIT_CURRENTBRANCHREMOTE: &str = "git.currentBranchRemote"; + /// `git.workingDirectoryContext` + pub const GIT_WORKINGDIRECTORYCONTEXT: &str = "git.workingDirectoryContext"; + /// `git.reposFromRemotes` + pub const GIT_REPOSFROMREMOTES: &str = "git.reposFromRemotes"; /// `managedSettings.read` pub const MANAGEDSETTINGS_READ: &str = "managedSettings.read"; /// `managedSettings.clearCache` @@ -250,6 +289,12 @@ pub mod rpc_methods { pub const SESSIONS_RELEASELOCK: &str = "sessions.releaseLock"; /// `sessions.enrichMetadata` pub const SESSIONS_ENRICHMETADATA: &str = "sessions.enrichMetadata"; + /// `sessions.createWorkspace` + pub const SESSIONS_CREATEWORKSPACE: &str = "sessions.createWorkspace"; + /// `sessions.loadWorkspace` + pub const SESSIONS_LOADWORKSPACE: &str = "sessions.loadWorkspace"; + /// `sessions.updateWorkspaceFields` + pub const SESSIONS_UPDATEWORKSPACEFIELDS: &str = "sessions.updateWorkspaceFields"; /// `sessions.reloadPluginHooks` pub const SESSIONS_RELOADPLUGINHOOKS: &str = "sessions.reloadPluginHooks"; /// `sessions.loadDeferredRepoHooks` @@ -272,8 +317,25 @@ pub mod rpc_methods { pub const SESSIONS_CONFIGURESESSIONEXTENSIONS: &str = "sessions.configureSessionExtensions"; /// `agentRegistry.spawn` pub const AGENTREGISTRY_SPAWN: &str = "agentRegistry.spawn"; - /// `accounts.acquireEntraToken` - pub const ACCOUNTS_ACQUIREENTRATOKEN: &str = "accounts.acquireEntraToken"; + /// `connectors.getCapabilities` + pub const CONNECTORS_GETCAPABILITIES: &str = "connectors.getCapabilities"; + /// `connectors.getAccounts` + pub const CONNECTORS_GETACCOUNTS: &str = "connectors.getAccounts"; + /// `connectors.list` + pub const CONNECTORS_LIST: &str = "connectors.list"; + /// `connectors.refresh` + pub const CONNECTORS_REFRESH: &str = "connectors.refresh"; + /// `session.providers.getCatalog` + pub const SESSION_PROVIDERS_GETCATALOG: &str = "session.providers.getCatalog"; + /// `session.providers.discover` + pub const SESSION_PROVIDERS_DISCOVER: &str = "session.providers.discover"; + /// `session.providers.getStatus` + pub const SESSION_PROVIDERS_GETSTATUS: &str = "session.providers.getStatus"; + /// `session.providers.models.list` + pub const SESSION_PROVIDERS_MODELS_LIST: &str = "session.providers.models.list"; + /// `session.providers.models.prepareConfiguration` + pub const SESSION_PROVIDERS_MODELS_PREPARECONFIGURATION: &str = + "session.providers.models.prepareConfiguration"; /// `session.suspend` pub const SESSION_SUSPEND: &str = "session.suspend"; /// `session.send` @@ -520,8 +582,12 @@ pub mod rpc_methods { pub const SESSION_SKILLS_RELOAD: &str = "session.skills.reload"; /// `session.skills.ensureLoaded` pub const SESSION_SKILLS_ENSURELOADED: &str = "session.skills.ensureLoaded"; + /// `session.mcp.setConnectedIdeInfo` + pub const SESSION_MCP_SETCONNECTEDIDEINFO: &str = "session.mcp.setConnectedIdeInfo"; /// `session.mcp.list` pub const SESSION_MCP_LIST: &str = "session.mcp.list"; + /// `session.mcp.listConfigured` + pub const SESSION_MCP_LISTCONFIGURED: &str = "session.mcp.listConfigured"; /// `session.mcp.listTools` pub const SESSION_MCP_LISTTOOLS: &str = "session.mcp.listTools"; /// `session.mcp.enable` @@ -566,6 +632,8 @@ pub mod rpc_methods { pub const SESSION_MCP_OAUTH_PREPARELOGIN: &str = "session.mcp.oauth.prepareLogin"; /// `session.mcp.oauth.login` pub const SESSION_MCP_OAUTH_LOGIN: &str = "session.mcp.oauth.login"; + /// `session.mcp.oauth.complete` + pub const SESSION_MCP_OAUTH_COMPLETE: &str = "session.mcp.oauth.complete"; /// `session.mcp.oauth.probe` pub const SESSION_MCP_OAUTH_PROBE: &str = "session.mcp.oauth.probe"; /// `session.mcp.oauth.cancelLogin` @@ -593,12 +661,18 @@ pub mod rpc_methods { pub const SESSION_MCP_RESOURCES_LIST: &str = "session.mcp.resources.list"; /// `session.mcp.resources.listTemplates` pub const SESSION_MCP_RESOURCES_LISTTEMPLATES: &str = "session.mcp.resources.listTemplates"; + /// `session.mcp.prompts.list` + pub const SESSION_MCP_PROMPTS_LIST: &str = "session.mcp.prompts.list"; + /// `session.mcp.prompts.get` + pub const SESSION_MCP_PROMPTS_GET: &str = "session.mcp.prompts.get"; /// `session.diagnostics.configure` pub const SESSION_DIAGNOSTICS_CONFIGURE: &str = "session.diagnostics.configure"; /// `session.diagnostics.read` pub const SESSION_DIAGNOSTICS_READ: &str = "session.diagnostics.read"; /// `session.connectors.getCapabilities` pub const SESSION_CONNECTORS_GETCAPABILITIES: &str = "session.connectors.getCapabilities"; + /// `session.connectors.getAccount` + pub const SESSION_CONNECTORS_GETACCOUNT: &str = "session.connectors.getAccount"; /// `session.connectors.getStatus` pub const SESSION_CONNECTORS_GETSTATUS: &str = "session.connectors.getStatus"; /// `session.connectors.list` @@ -711,8 +785,12 @@ pub mod rpc_methods { pub const SESSION_UI_ELICITATION: &str = "session.ui.elicitation"; /// `session.ui.handlePendingElicitation` pub const SESSION_UI_HANDLEPENDINGELICITATION: &str = "session.ui.handlePendingElicitation"; + /// `session.ui.handleHumanAskUser` + pub const SESSION_UI_HANDLEHUMANASKUSER: &str = "session.ui.handleHumanAskUser"; /// `session.ui.handlePendingUserInput` pub const SESSION_UI_HANDLEPENDINGUSERINPUT: &str = "session.ui.handlePendingUserInput"; + /// `session.ui.handleHumanUserInput` + pub const SESSION_UI_HANDLEHUMANUSERINPUT: &str = "session.ui.handleHumanUserInput"; /// `session.ui.handlePendingSampling` pub const SESSION_UI_HANDLEPENDINGSAMPLING: &str = "session.ui.handlePendingSampling"; /// `session.ui.handlePendingAutoModeSwitch` @@ -723,6 +801,8 @@ pub mod rpc_methods { "session.ui.handlePendingSessionLimitsExhausted"; /// `session.ui.handlePendingExitPlanMode` pub const SESSION_UI_HANDLEPENDINGEXITPLANMODE: &str = "session.ui.handlePendingExitPlanMode"; + /// `session.ui.handleHumanExitPlanMode` + pub const SESSION_UI_HANDLEHUMANEXITPLANMODE: &str = "session.ui.handleHumanExitPlanMode"; /// `session.ui.registerDirectAutoModeSwitchHandler` pub const SESSION_UI_REGISTERDIRECTAUTOMODESWITCHHANDLER: &str = "session.ui.registerDirectAutoModeSwitchHandler"; @@ -935,8 +1015,12 @@ pub mod rpc_methods { pub const TASKS_CANCEL: &str = "tasks.cancel"; /// `sessionFs.readFile` pub const SESSIONFS_READFILE: &str = "sessionFs.readFile"; + /// `sessionFs.readFileBytes` + pub const SESSIONFS_READFILEBYTES: &str = "sessionFs.readFileBytes"; /// `sessionFs.writeFile` pub const SESSIONFS_WRITEFILE: &str = "sessionFs.writeFile"; + /// `sessionFs.writeFileBytes` + pub const SESSIONFS_WRITEFILEBYTES: &str = "sessionFs.writeFileBytes"; /// `sessionFs.appendFile` pub const SESSIONFS_APPENDFILE: &str = "sessionFs.appendFile"; /// `sessionFs.exists` @@ -1573,6 +1657,25 @@ pub struct UserAuthInfo { pub r#type: UserAuthInfoType, } +/// An interactive account whose model provider owns its credentials. It carries no GitHub credential. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountAuthInfo { + /// Host coordinate owned by the account's model provider. + pub host: String, + /// Login identifying the provider-owned account. + pub login: String, + /// Provider-owned account authentication. + pub r#type: AccountAuthInfoType, +} + /// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. /// ///
@@ -2300,6 +2403,45 @@ pub struct AgentReloadResult { pub agents: Vec, } +/// The models a custom agent asks for, and the models actually available. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsCustomAgentInitialModelDecisionParams { + /// The agent's declared `model:` entry, serialized. A single name or an ordered list of acceptable names. + pub agent_models_json: String, + /// The models available to this session, serialized in the shape the model list carries. + pub available_models_json: String, +} + +/// The model to switch to, and the warning to show when the agent's preference could not be met. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsCustomAgentInitialModelDecisionResult { + /// The reasoning effort attached to the selected model preference. Absent when that preference does not specify an effort. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + /// The first available model that matches the agent's preferences. Absent when none of the requested models is available. + #[serde(skip_serializing_if = "Option::is_none")] + pub target_model: Option, + /// What to tell the user about an unmet preference. Absent when the preference was met. A warning with no `targetModel` means the agent's models are all unavailable. + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option, +} + /// Optional project paths to include in agent discovery. /// ///
@@ -2366,6 +2508,140 @@ pub struct AgentSetPromptRequest { pub prompt: String, } +/// The feature flags to evaluate shipped agents against. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetAvailableBuiltinsRequest { + /// The surface asking, which gates agents that only apply to one client. Omit or pass null to apply no client filter. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// Feature flag values keyed by name, evaluated with the runtime's truthiness rules. Omit or pass null for no flags. + #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + /// Flag overrides keyed by name. A null entry uses the corresponding base flag; false explicitly disables it. Omit or pass null for no overrides. + #[serde(skip_serializing_if = "Option::is_none")] + pub overrides: Option>, +} + +/// A shipped agent, named and described. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct BuiltinAgentSummary { + /// One-line description of what the agent does. + pub description: String, + /// The agent name, as it appears in `getBuiltins`. + pub name: String, +} + +/// The shipped agents available under the requested flags. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetAvailableBuiltinsResult { + /// Available shipped agents, in the runtime's own order. + #[doc(hidden)] + pub(crate) agents: Vec, +} + +/// The shipped agent whose definition to load. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinDefinitionRequest { + /// The agent name, which must be one of `getBuiltins`'s `yamlBasedNames`. A name outside that list is special-cased in code and has no definition, and is reported as an error rather than as an empty definition. + pub name: String, +} + +/// One shipped agent's definition. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinDefinitionResult { + /// The agent's definition, serialized as JSON. It carries the authored keys plus the runtime's projected `__nativeCustomAgent` view of the same agent. It is a string rather than an object because the runtime parses it with the agent schema's tolerant shape, which accepts keys this contract does not name. + pub definition_json: String, +} + +/// The shipped agent whose listing entry to load. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinListingDefinitionRequest { + /// The agent name, taken from `getAvailableBuiltins`. Unlike `getBuiltinDefinition`, the agent that `getBuiltins` reports as special-cased rather than YAML-based is answered here too, from its in-code definition. + pub name: String, +} + +/// One shipped agent, projected for a listing. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinListingDefinitionResult { + /// The agent projected as a custom agent, serialized as JSON. It is a string rather than an object for the same reason as `getBuiltinDefinition`: the runtime parses the underlying definition with the agent schema's tolerant shape, which accepts keys this contract does not name. + pub definition_json: String, +} + +/// The agents this runtime ships, named so a consumer can tell them apart from authored ones. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgentsGetBuiltinsResult { + /// The subset of `names` a user is allowed to turn off. A shipped agent outside this list is always active and a client should not offer a toggle for it. + pub disableable_names: Vec, + /// Every agent name this runtime ships. + pub names: Vec, + /// The subset of `names` defined by a shipped YAML definition. The remainder are special-cased in code and have no definition to load. + pub yaml_based_names: Vec, +} + /// Optional project paths to include when enumerating agent discovery directories. /// ///
@@ -2952,6 +3228,46 @@ pub struct AuthIdentity { pub r#type: AuthInfoType, } +/// Credential-free identity metadata. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthIdentityMetadata { + /// Identity host. + pub host: String, + /// User login. + pub login: String, + /// Authentication type. + pub r#type: AuthInfoType, +} + +/// A credential-free account choice after sign-in. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthLoginAccount { + /// Host coordinate owned by the selected account's provider. + pub host: String, + /// Provider kind that owns this account choice. + pub kind: AccountKind, + /// Human-readable login for the account choice. + pub login: String, + /// Opaque identifier supplied to the next login step to select this account. + pub selection_id: String, +} + /// Advance an in-flight login flow, optionally fulfilling an input-required step. /// ///
@@ -3045,7 +3361,7 @@ pub struct AuthLoginStepNeedsInteraction { pub kind: AuthLoginStepNeedsInteractionKind, } -/// Terminal result of an interactive login flow. +/// Result of an interactive login flow. Pending consent or account selection is not terminal. /// ///
/// @@ -3056,13 +3372,16 @@ pub struct AuthLoginStepNeedsInteraction { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthLoginResultDto { + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + #[serde(skip_serializing_if = "Option::is_none")] + pub accounts: Option>, /// Host that was signed in, when completed. #[serde(skip_serializing_if = "Option::is_none")] pub host: Option, /// Login that was signed in, when completed. #[serde(skip_serializing_if = "Option::is_none")] pub login: Option, - /// Terminal disposition of the login. + /// Current disposition of the login, including pending user decisions. pub status: AuthLoginResultStatus, } @@ -3078,7 +3397,7 @@ pub struct AuthLoginResultDto { pub struct AuthLoginStepCompleted { /// Login flow step variant discriminator. pub kind: AuthLoginStepCompletedKind, - /// The terminal login result. + /// Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. pub result: AuthLoginResultDto, } @@ -3143,6 +3462,9 @@ pub struct AuthReadValueActiveAccount { /// The active account, or absent when not logged in. #[serde(skip_serializing_if = "Option::is_none")] pub account: Option, + /// Credential-free identity metadata for the active account, including resolved Copilot user information when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_info: Option, /// Account read-datum variant discriminator. pub kind: AuthReadValueActiveAccountKind, } @@ -5626,6 +5948,12 @@ pub struct ConnectorCapabilities { pub max_poll_interval_ms: i64, /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. pub opaque_account_selection: bool, + /// Whether getAccount is supported. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_account_selection: Option, + /// Whether reconcile accepts forceConnectorName. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub targeted_reconcile: Option, } /// Credential-free Connector catalog entry. @@ -5644,12 +5972,21 @@ pub struct ConnectorCatalogEntry { pub description: Option, /// Untrusted display label from the service. pub display_name: String, + /// Optional catalog logo. + #[serde(skip_serializing_if = "Option::is_none")] + pub logo: Option, /// Canonical Connector name used by lifecycle methods. pub name: String, + /// Optional catalog release tag. + #[serde(skip_serializing_if = "Option::is_none")] + pub release_tag: Option, /// Opaque stable runtime IDs currently projected into the session for this Connector. pub runtime_server_ids: Vec, /// Current authoritative service connection state. pub status: ConnectorCatalogStatus, + /// Optional catalog tier. + #[serde(skip_serializing_if = "Option::is_none")] + pub tier: Option, } /// Validated Connector catalog snapshot cached by the session. @@ -5828,6 +6165,147 @@ pub struct ConnectorDisconnectResult { pub status: ConnectorStatus, } +/// Account metadata. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAuthInfo { + /// Host. + pub host: String, + /// Login. + pub login: String, + /// Authentication type. + pub r#type: AuthInfoType, +} + +/// Eligible account. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccount { + /// Opaque account ID. + pub account_id: String, + /// Account metadata. + pub auth_info: ConnectorDiscoveryAuthInfo, +} + +/// Eligible accounts. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccountList { + /// Eligible accounts. + pub accounts: Vec, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, +} + +/// Selected account. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryAccountRequest { + /// Opaque account ID. + pub account_id: String, +} + +/// Feature availability. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCapabilities { + /// API version. + pub api_version: i64, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, + /// Whether results are cached. + pub conditional_cache: bool, + /// Whether accounts are selected by opaque ID. + pub opaque_account_selection: bool, +} + +/// Entry. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCatalogEntry { + /// Description. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Display name. + pub display_name: String, + /// Logo. + #[serde(skip_serializing_if = "Option::is_none")] + pub logo: Option, + /// Name. + pub name: String, + /// Release tag. + #[serde(skip_serializing_if = "Option::is_none")] + pub release_tag: Option, + /// Status. + pub status: ConnectorCatalogStatus, + /// Tier. + #[serde(skip_serializing_if = "Option::is_none")] + pub tier: Option, +} + +/// Entries for the selected account. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDiscoveryCatalogResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + /// Requests authoritative Connector-to-MCP reconciliation for the pinned account. /// ///
@@ -5846,6 +6324,68 @@ pub struct ConnectorReconcileRequest { pub refresh_catalog: Option, } +/// Extensible [`ConnectorReconcileRequest`], including inputs added after it was published. +/// +/// Required inputs are [`ConnectorReconcileOptions::new`] arguments; optional inputs have fluent setters. +/// Input-only: it serialises to the flat wire request and is not deserialisable. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorReconcileOptions { + #[serde(flatten)] + legacy: ConnectorReconcileRequest, + #[serde(skip_serializing_if = "Option::is_none")] + force_connector_name: Option, +} + +impl ConnectorReconcileOptions { + /// Creates options with the required inputs. + pub fn new(account_id: impl Into) -> Self { + Self { + legacy: ConnectorReconcileRequest { + account_id: account_id.into(), + refresh_catalog: None, + }, + force_connector_name: None, + } + } + + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + pub fn refresh_catalog(mut self, value: bool) -> Self { + self.legacy.refresh_catalog = Some(value); + self + } + + /// Optional Connector name to reinitialize. Requires the targetedReconcile capability. + pub fn force_connector_name(mut self, value: impl Into) -> Self { + self.force_connector_name = Some(value.into()); + self + } +} + +/// Session account selection. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorSessionAccount { + /// Opaque session-scoped account selection ID. + pub account_id: String, + /// Credential-free identity metadata. + pub auth_info: AuthIdentityMetadata, +} + /// Remote session connection parameters. /// ///
@@ -6005,6 +6545,9 @@ pub struct CurrentModel { /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. #[serde(skip_serializing_if = "Option::is_none")] pub pending_auto_tier: Option, + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_model_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -6042,6 +6585,45 @@ pub struct CurrentToolMetadata { pub namespaced_name: Option, } +/// Result of one customization reload component. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomizationReloadOutcome { + /// Reason for a skipped component or description of a failure, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// Whether the component reloaded, was skipped, or failed + pub status: CustomizationReloadStatus, + /// Component whose reload was attempted or skipped + pub subsystem: CustomizationReloadSubsystem, +} + +/// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomizationsReloadResult { + /// Errors from any component that could not be refreshed + pub errors: Vec, + /// Outcome of each component in reload order; a skipped component was not configured or loaded + pub outcomes: Vec, + /// Warnings from skill discovery + pub warnings: Vec, +} + /// A file included in the session debug bundle. /// ///
@@ -6607,7 +7189,7 @@ pub struct DiscoveredMcpServer { pub r#type: Option, } -/// Slash-prefixed command string to enqueue for FIFO processing. +/// Vision-specific limits /// ///
/// @@ -6617,15 +7199,47 @@ pub struct DiscoveredMcpServer { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EnqueueCommandParams { - /// 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. - pub command: String, - /// Optional user-facing text for the queue row. The command string is shown when omitted. +pub struct ModelCapabilitiesLimitsVision { + /// Maximum image size in bytes + #[serde(rename = "max_prompt_image_size")] + pub max_prompt_image_size: i64, + /// Maximum number of images per prompt + #[serde(rename = "max_prompt_images")] + pub max_prompt_images: i64, + /// MIME types the model accepts + #[serde(rename = "supported_media_types")] + pub supported_media_types: Vec, +} + +/// Token limits for prompts, outputs, and context window +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelCapabilitiesLimits { + /// Maximum total context window size in tokens + #[serde( + rename = "max_context_window_tokens", + skip_serializing_if = "Option::is_none" + )] + pub max_context_window_tokens: Option, + /// Maximum number of output/completion tokens + #[serde(rename = "max_output_tokens", skip_serializing_if = "Option::is_none")] + pub max_output_tokens: Option, + /// Maximum number of prompt/input tokens + #[serde(rename = "max_prompt_tokens", skip_serializing_if = "Option::is_none")] + pub max_prompt_tokens: Option, + /// Vision-specific limits #[serde(skip_serializing_if = "Option::is_none")] - pub display_text: Option, + pub vision: Option, } -/// OneAuth token request supplied by a trusted host application. +/// Feature flags indicating what the model supports /// ///
/// @@ -6635,42 +7249,205 @@ pub struct EnqueueCommandParams { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EntraTokenAcquireRequest { - /// Previously rejected token that OneAuth must bypass during renewal. +pub struct ModelCapabilitiesSupports { + /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). + #[serde(rename = "adaptive_thinking", skip_serializing_if = "Option::is_none")] + pub adaptive_thinking: Option, + /// Whether this model supports reasoning effort configuration + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + /// Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. + #[serde(skip_serializing_if = "Option::is_none")] + pub thinking: Option, + /// Whether this model supports canonical tool calling #[serde(skip_serializing_if = "Option::is_none")] - pub access_token_to_renew: Option, - /// Public client application id. - pub client_id: String, - /// Whether the broker may show interaction. - pub interaction: EntraTokenInteraction, - /// Broker redirect URI registered for the client. Required: the OneAuth broker validates a non-empty, registered redirect URI for the public client (MSAL broker registration), so this is not a browser-flow vestige and cannot be omitted. - pub redirect_uri: String, - /// Exact delegated scopes to request. - pub scopes: Vec, - /// Tenant id or tenant selector, such as common or organizations. - pub tenant_id: String, + pub tool_calls: Option, + /// Whether this model supports vision/image input + #[serde(skip_serializing_if = "Option::is_none")] + pub vision: Option, } +/// Model capabilities and limits +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EntraTokenAcquireResultOk { - /// Opaque access token. - pub access_token: String, - /// Opaque OneAuth account id, when supplied by the broker. +pub struct ModelCapabilities { + /// Token limits for prompts, outputs, and context window #[serde(skip_serializing_if = "Option::is_none")] - pub account_id: Option, - /// Expiry as milliseconds since Unix epoch, when supplied by OneAuth. + pub limits: Option, + /// Feature flags indicating what the model supports + #[serde(skip_serializing_if = "Option::is_none")] + pub supports: Option, +} + +/// Provider-reported model artifact metadata. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelArtifactDetails { + /// Provider-reported model architecture. + #[serde(skip_serializing_if = "Option::is_none")] + pub architecture: Option, + /// Provider-reported model families. + #[serde(skip_serializing_if = "Option::is_none")] + pub families: Option>, + /// Primary model family. + #[serde(skip_serializing_if = "Option::is_none")] + pub family: Option, + /// Artifact format, such as `gguf`. + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + /// Provider-reported parameter count label. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_size: Option, + /// Provider-reported quantization label. + #[serde(skip_serializing_if = "Option::is_none")] + pub quantization: Option, + /// Provider-reported tokenizer. + #[serde(skip_serializing_if = "Option::is_none")] + pub tokenizer: Option, +} + +/// Attribution for the adapter that produced a provider row. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderProvenance { + /// Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. + pub adapter_id: String, + /// Human-readable contributor name, not the adapter display name. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_display_name: Option, + /// Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_id: Option, + /// Descriptive provider family that produced this row; not a routing key. + pub provider_kind: String, + /// Kind of component that supplied the adapter. + pub source: ModelProviderProvenanceSource, +} + +/// A non-fatal provider observation warning. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderWarning { + /// Machine-readable warning code. + pub code: String, + /// Human-readable warning message. + pub message: String, +} + +/// A model offered for agent conversations. Missing capability metadata does not disqualify a candidate. Models known to be incompatible, such as embedding-only models, are excluded by the adapter. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DiscoveredModel { + /// Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. + pub capabilities: ModelCapabilities, + /// Provider-reported model artifact details. + pub details: ModelArtifactDetails, + /// Provider-reported artifact digest. + #[serde(skip_serializing_if = "Option::is_none")] + pub digest: Option, + /// Provider-native model identifier. + pub id: String, + /// Provider-reported last-modified timestamp. + #[serde(skip_serializing_if = "Option::is_none")] + pub modified_at: Option, + /// Provider-reported display name. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Attribution for the adapter that produced this model row. + pub provenance: ModelProviderProvenance, + /// Provider-reported artifact size in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size_bytes: Option, + /// Non-fatal warnings encountered while enriching this model. + pub warnings: Vec, +} + +/// Typed provider-operation outcome. Use the code for control flow and the optional message for display. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderOperationOutcome { + /// Machine-readable operation outcome. + pub code: ModelProviderOperationOutcomeCode, + /// Human-readable detail for non-success outcomes. #[serde(skip_serializing_if = "Option::is_none")] - pub expires_on_timestamp: Option, - /// OneAuth token acquisition outcome discriminator. - pub status: EntraTokenAcquireResultOkStatus, + pub message: Option, } +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EntraTokenAcquireResultInteractionRequired { - /// OneAuth token acquisition outcome discriminator. - pub status: EntraTokenAcquireResultInteractionRequiredStatus, +pub struct DiscoveredModelList { + /// Provider-native models in provider order. + pub models: Vec, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, +} + +/// Slash-prefixed command string to enqueue for FIFO processing. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnqueueCommandParams { + /// 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. + pub command: String, + /// Optional user-facing text for the queue row. The command string is shown when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_text: Option, } /// Hosting capabilities and session capacity advertised by an environment. @@ -7423,6 +8200,191 @@ pub struct FolderTrustCheckResult { pub trusted: bool, } +/// The remote the checked-out branch tracks. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitCurrentBranchRemoteResult { + /// Name of the tracked remote. Reports `origin` whenever the working tree has no tracking configuration to read, including on a detached HEAD, so this is never null and never empty. + pub remote: String, +} + +/// Working-tree path a git query applies to. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitCwdRequest { + /// Absolute path to a directory inside the git working tree to query. + pub cwd: String, +} + +/// A GitHub login the authenticated user may act as: their own account, or an organization they belong to. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnerOption { + /// The owner's GitHub login. + pub login: String, + /// Which kind of owner this is. The authenticated user's own account is always reported as `user`. + pub r#type: String, +} + +/// The owner listing to abandon. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersCancelRequest { + /// Request id the listing was started with. + pub request_id: i64, +} + +/// Whether the id named a running owner listing. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersCancelResult { + /// True when a listing with the id was running and the cancel stopped it. False when the id was never registered, was registered but unused, was released after being abandoned, or its listing had ended. An unused id is released and cannot start a later listing. + pub canceled: bool, +} + +/// Credential to list owners under, and the request id that makes the listing cancellable. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersListRequest { + /// The credential the listing runs under, carried opaquely because its shape is the host's own and the runtime only resolves a token and a GitHub host from it. No credential travels: this selects one the runtime already holds. + pub auth_info: serde_json::Value, + /// Request id from `gitHubOwners.nextRequestId`. An id that was never registered, canceled before use, released after being abandoned, or already used is refused rather than silently running uncancellable. + pub request_id: i64, +} + +/// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersListResult { + /// Why no owners could be listed, phrased for a user. Present when the listing failed in a way the caller should render rather than raise. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// The owners, on success: the authenticated user first, then the organizations they belong to. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) owners: Option>, + /// A malformed request or an unreadable credential, which the caller raises instead of rendering. Kept a field rather than a dispatch error so it stays distinct from `message`, which the caller renders. + #[serde(skip_serializing_if = "Option::is_none")] + pub throw_error: Option, + /// A line the caller should log. Present only alongside `message`, and only for failures worth recording. + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option, +} + +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersRequestIdResult { + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + pub request_id: i64, +} + +/// Working-tree path whose owning GitHub repository should be resolved. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubRepositoryAtPathRequest { + /// Absolute path to a directory inside the git working tree to resolve. + pub path: String, +} + +/// Owner, name, and host of a GitHub repository, as resolved from a git remote URL. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitHubRepositoryIdentity { + /// Host the remote points at, for example `github.com` or a GitHub Enterprise hostname. + pub host: String, + /// Repository name, without the owner prefix or the `.git` suffix. + pub name: String, + /// Repository owner login (user or organization). + pub owner: String, +} + +/// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubRepositoryAtPathResult { + /// Resolved repository identity, or null when the selected remote resolves to no GitHub host. + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, +} + /// Client environment metadata describing the process that produced a telemetry event. /// ///
@@ -7581,6 +8543,230 @@ pub struct GitHubTokenAcquireResultCancelled { pub kind: GitHubTokenAcquireResultCancelledKind, } +/// A GitHub repository one of a working tree's remotes points at. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitRemoteRepository { + /// GitHub host serving the repository, which is not `github.com` for a GitHub Enterprise remote. + pub host: String, + /// Repository name, without the owner. + pub name: String, + /// Account or organization owning the repository. + pub owner: String, + /// Name of the first remote that produced this distinct repository entry, such as `origin` or `upstream`. + pub remote_name: String, +} + +/// Git working tree whose GitHub remotes should be listed. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitReposFromRemotesRequest { + /// Absolute path to the root of the git working tree. + pub git_root: String, +} + +/// The GitHub repositories a working tree's remotes point at. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitReposFromRemotesResult { + /// One entry per distinct GitHub repository, in the order git reports the first remote for each repository. Empty when no remote points at a GitHub host, which a caller should read as `not connected to GitHub`. Failing to read the remotes is an error, not an empty list. + #[doc(hidden)] + pub(crate) repositories: Vec, +} + +/// Selects the configuration directory whose machine-wide state to read. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateLoadForConfigDirRequest { + /// Copilot configuration directory to read the state document from, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to read the directory the server resolved for itself. + #[serde(skip_serializing_if = "Option::is_none")] + pub config_dir: Option, +} + +/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstalledPlugin { + /// Path where the plugin is cached locally + #[serde(rename = "cache_path", skip_serializing_if = "Option::is_none")] + pub cache_path: Option, + /// Whether the plugin is currently enabled + pub enabled: bool, + /// Installation timestamp + #[serde(rename = "installed_at")] + pub installed_at: String, + /// 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. + #[serde(rename = "installed_from", skip_serializing_if = "Option::is_none")] + pub installed_from: Option, + /// Marketplace the plugin came from (empty string for direct repo installs) + pub marketplace: String, + /// Plugin name + pub name: String, + /// Source for direct repo installs (when marketplace is empty) + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. + #[serde(rename = "source_sha", skip_serializing_if = "Option::is_none")] + pub source_sha: Option, + /// Version installed (if available) + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +/// An account the host has signed in to, identified by the server it lives on and the login it uses there. The same person can appear more than once when they use both github.com and an Enterprise server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct LoggedInUser { + /// Source account this account was derived from, when one was recorded. + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option, + /// Host the account belongs to, such as `github.com` or an Enterprise server. + pub host: String, + /// Account kind, when the host recorded one. Consumers must tolerate new strings. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// Account login on that host. + pub login: String, +} + +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateLoadResult { + /// Whether the user has answered the prompt suggesting they install the desktop app. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_install_nudge_responded: Option, + /// Whether the app tip has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_tip_shown: Option, + /// Terminals the user has already been asked to set up, so the host does not ask twice. + #[serde(skip_serializing_if = "Option::is_none")] + pub asked_setup_terminals: Option>, + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_feedback_last_prompted_at: Option, + /// When the host first ran on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub first_launch_at: Option, + /// Plugins installed on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed_plugins: Option>, + /// Account used for the most recent sign-in. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) last_logged_in_user: Option, + /// Every account the host has signed in to on this machine. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) logged_in_users: Option>, + /// Whether the one-off cleanup of stored reasoning summaries has run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summaries_cleanup_done: Option, + /// Models the user selected recently, most recent first. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_model_ids: Option>, + /// Whether the user declined to trust the sandbox credential proxy CA. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_credential_proxy_ca_declined: Option, + /// Whether the sandbox onboarding has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_onboarding_shown: Option, + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff: Option, + /// Whether the user was recognized as GitHub staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_github: Option, + /// When the staff-only log level migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_log_level_migration_at: Option, + /// Whether the user was recognized as Microsoft staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_microsoft: Option, + /// When the staff-only model reset last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_model_reset_at: Option, + /// When the staff-only update channel migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_update_channel_migration_at: Option, + /// Folders where the user declined the init prompt, so it stays hidden there. + #[serde(skip_serializing_if = "Option::is_none")] + pub suppress_init_folders: Option>, + /// Folders the user has marked as trusted. + #[serde(skip_serializing_if = "Option::is_none")] + pub trusted_folders: Option>, +} + +/// A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateWriteKeyRequest { + /// Copilot configuration directory to write the state document in, taking precedence over the server's own `COPILOT_HOME` and default home. Omit it, or pass an empty string, to write the directory the server resolved for itself. Mirrors `globalState.loadForConfigDir`, so a caller can read and write the same directory. + #[serde(skip_serializing_if = "Option::is_none")] + pub config_dir: Option, + /// Top-level key to write, named as it appears in the result of `globalState.load`. It must be one of the writable keys that `globalState.writeKey` lists. + pub key: String, + /// Value to store for the key. Omit it, or pass null, to remove the key instead. + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + /// Pending external tool call request ID, with the tool result or an error describing why it failed. /// ///
@@ -9048,43 +10234,6 @@ pub struct InstallationConfirmationResponse { pub review_fingerprint: String, } -/// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InstalledPlugin { - /// Path where the plugin is cached locally - #[serde(rename = "cache_path", skip_serializing_if = "Option::is_none")] - pub cache_path: Option, - /// Whether the plugin is currently enabled - pub enabled: bool, - /// Installation timestamp - #[serde(rename = "installed_at")] - pub installed_at: String, - /// 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. - #[serde(rename = "installed_from", skip_serializing_if = "Option::is_none")] - pub installed_from: Option, - /// Marketplace the plugin came from (empty string for direct repo installs) - pub marketplace: String, - /// Plugin name - pub name: String, - /// Source for direct repo installs (when marketplace is empty) - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - /// Per-plugin source fingerprint (a SHA-256 hash of the plugin's catalog source spec plus its resolved source subtree — NOT a Git commit SHA) captured at marketplace install/update time. Auto-update compares it against the freshly recomputed fingerprint to detect a content change that does not bump the version. Absent for pre-existing installs and for direct (non-marketplace) installs. - #[serde(rename = "source_sha", skip_serializing_if = "Option::is_none")] - pub source_sha: Option, - /// Version installed (if available) - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, -} - /// Information about an installed plugin tracked in global state. /// ///
@@ -10636,6 +11785,71 @@ pub struct McpConfigUpdateRequest { pub name: String, } +/// Observational state for a matching already materialized MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerState { + /// Observed connection error, when the materialized server failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Observed connection status. This is not a configuration or readiness guarantee. + pub status: McpServerStatus, +} + +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServer { + /// Human-readable display name supplied by configuration. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Whether this configured server is enabled after session configuration and policy filtering. + pub enabled: bool, + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + #[serde(skip_serializing_if = "Option::is_none")] + pub live: Option, + /// Server name (config key) + pub name: String, + /// Configuration provenance: user, workspace, plugin, builtin, or managed. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// Plugin name that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin: Option, + /// Plugin version that provided this server, when source is plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_plugin_version: Option, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfiguredServerList { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Credential-free authentication identity used to configure GitHub MCP. /// ///
@@ -11547,6 +12761,23 @@ pub struct McpOauthCancelLoginResult { pub cancelled: bool, } +/// Host-delivered callback for a runtime-managed MCP OAuth login. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpOauthCompleteRequest { + /// Opaque identifier returned by session.mcp.oauth.login for the pending external callback. + pub authorization_id: String, + /// Full externally visible HTTPS callback URL received by the host, including the authorization response query parameters. Applications behind a reverse proxy must reconstruct the public URL rather than passing an internal proxy URL. + pub callback_url: String, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpOauthPendingRequestResponseToken { @@ -11601,7 +12832,7 @@ pub struct McpOauthHandlePendingResult { pub success: bool, } -/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. +/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. /// ///
/// @@ -11654,6 +12885,8 @@ pub struct McpOauthLoginOptions { #[serde(flatten)] legacy: McpOauthLoginRequest, #[serde(skip_serializing_if = "Option::is_none")] + redirect_uri: Option, + #[serde(skip_serializing_if = "Option::is_none")] login_id: Option, #[serde(skip_serializing_if = "Option::is_none")] expected_installation_id: Option, @@ -11673,6 +12906,7 @@ impl McpOauthLoginOptions { public_client: None, grant_type: None, }, + redirect_uri: None, login_id: None, expected_installation_id: None, } @@ -11720,6 +12954,12 @@ impl McpOauthLoginOptions { self } + /// Optional externally visible HTTPS redirect URI for a host-managed callback endpoint. When supplied, the runtime still owns discovery, PKCE, token exchange, persistence, and reconnect, but does not bind a loopback listener or terminate HTTPS. The URI must not contain query parameters or a fragment and must be registered for the selected CIMD, DCR, or static OAuth client. + pub fn redirect_uri(mut self, value: impl Into) -> Self { + self.redirect_uri = Some(value.into()); + self + } + /// Required for owned login. Consumes the exact prepareLogin handle once. /// Set forceReauth and display options during preparation, not consumption. pub fn login_id(mut self, value: impl Into) -> Self { @@ -11745,7 +12985,10 @@ impl McpOauthLoginOptions { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpOauthLoginResult { - /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. When present, the runtime starts the callback listener before returning and continues the flow in the background; completion is signaled via session.mcp_server_status_changed. + /// Opaque authorization identifier returned only for a host-managed redirect URI. The runtime also sends it as the OAuth state value, so the callback endpoint can read state and pass it with the full callback URL to session.mcp.oauth.complete. + #[serde(skip_serializing_if = "Option::is_none")] + pub authorization_id: Option, + /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. For the default loopback flow, the runtime starts its listener before returning. With redirectUri, the host receives the callback and completes it through session.mcp.oauth.complete. The runtime continues the flow in the background and signals completion via session.mcp_server_status_changed. #[serde(skip_serializing_if = "Option::is_none")] pub authorization_url: Option, /// Runtime-issued owned flow identity; never a server name or installation operation ID. @@ -12201,6 +13444,202 @@ pub struct McpPrepareInstallRequest { pub source: McpServerCardReference, } +/// An argument accepted by an MCP prompt. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptArgument { + /// Argument-level metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard argument fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Description of the argument + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Name of the argument + pub name: String, + /// Whether the argument is required; omission is distinct from false + #[serde(skip_serializing_if = "Option::is_none")] + pub required: Option, +} + +/// An MCP prompt icon with standard size hints and preserved non-standard fields. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptIcon { + /// Server-provided non-standard icon fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Icon MIME type, when known + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + /// Icon sizes, such as `48x48` or `any` + #[serde(skip_serializing_if = "Option::is_none")] + pub sizes: Option>, + /// Icon URI + pub src: String, + /// Theme hint for this icon + #[serde(skip_serializing_if = "Option::is_none")] + pub theme: Option, +} + +/// An MCP prompt descriptor. Server-provided non-standard fields are exposed under `additionalProperties`. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPrompt { + /// Prompt-level metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard descriptor fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Arguments accepted by the prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub arguments: Option>, + /// Description of what this prompt provides + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Icons associated with this prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub icons: Option>, + /// The programmatic name of the prompt + pub name: String, + /// Human-readable display title + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, +} + +/// An MCP prompt message with opaque JSON content preserved without flattening or content-type filtering. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptMessage { + /// Message-level metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard message fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// The original MCP content block, including nested metadata and unfamiliar content types + pub content: serde_json::Value, + /// The role of the message sender + pub role: McpPromptRole, +} + +/// MCP server, prompt name, and optional string-valued arguments. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptsGetRequest { + /// String-valued arguments to pass to the prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub arguments: Option>, + /// The programmatic name of the prompt + pub prompt_name: String, + /// Name of the MCP server hosting the prompt + pub server_name: String, +} + +/// Prompt messages returned by the MCP server without sending them to the model. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptsGetResult { + /// MCP result metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard result fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Description of the prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Ordered prompt messages + pub messages: Vec, +} + +/// MCP server whose prompts to enumerate. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptsListRequest { + /// Opaque MCP pagination cursor from a prior `nextCursor` value + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, + /// Name of the MCP server whose prompts to enumerate + pub server_name: String, +} + +/// One page of prompts advertised by the named MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPromptsListResult { + /// MCP result metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard result fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Opaque cursor for the next page, if the server has more prompts + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Prompts advertised by the server + pub prompts: Vec, +} + /// Registration parameters for an external MCP client. /// ///
@@ -12709,6 +14148,9 @@ pub struct McpServer { pub source_plugin_version: Option, /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured pub status: McpServerStatus, + /// Configured URL for an HTTP/SSE server, regardless of configuration source. Omitted for local and in-memory servers. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, } /// Authentication settings with optional redirect port configuration. @@ -13229,7 +14671,7 @@ pub struct MemoryConfiguration { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextAttributionResultContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -13276,7 +14718,7 @@ pub struct MetadataContextAttributionResultContextAttributionEntriesItem { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextAttributionResultContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: MetadataContextAttributionResultContextAttributionCategories, @@ -13286,13 +14728,13 @@ pub struct MetadataContextAttributionResultContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -13357,9 +14799,9 @@ pub struct MetadataContextHeaviestMessagesResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextInfoRequest { - /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + /// Requested output allowance to reserve against the combined context ceiling. Pass 0 to resolve the session's request cap, falling back to the model's advertised output limit. pub output_token_limit: i64, - /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + /// Advertised prompt allowance. Pass 0 to resolve the selected model and context tier from the session. pub prompt_token_limit: i64, /// Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. #[serde(skip_serializing_if = "Option::is_none")] @@ -13370,19 +14812,19 @@ pub struct MetadataContextInfoRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetadataContextInfoResultContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, /// Tokens consumed by the system prompt pub system_tokens: i64, @@ -13755,100 +15197,6 @@ pub struct ModelBilling { pub token_prices: Option, } -/// Vision-specific limits -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesLimitsVision { - /// Maximum image size in bytes - #[serde(rename = "max_prompt_image_size")] - pub max_prompt_image_size: i64, - /// Maximum number of images per prompt - #[serde(rename = "max_prompt_images")] - pub max_prompt_images: i64, - /// MIME types the model accepts - #[serde(rename = "supported_media_types")] - pub supported_media_types: Vec, -} - -/// Token limits for prompts, outputs, and context window -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesLimits { - /// Maximum total context window size in tokens - #[serde( - rename = "max_context_window_tokens", - skip_serializing_if = "Option::is_none" - )] - pub max_context_window_tokens: Option, - /// Maximum number of output/completion tokens - #[serde(rename = "max_output_tokens", skip_serializing_if = "Option::is_none")] - pub max_output_tokens: Option, - /// Maximum number of prompt/input tokens - #[serde(rename = "max_prompt_tokens", skip_serializing_if = "Option::is_none")] - pub max_prompt_tokens: Option, - /// Vision-specific limits - #[serde(skip_serializing_if = "Option::is_none")] - pub vision: Option, -} - -/// Feature flags indicating what the model supports -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilitiesSupports { - /// Resolved Anthropic adaptive-thinking capability — unsupported / optional / required / adaptive_only. 'required' models reject thinking.type='enabled' with HTTP 400 but still accept 'disabled' (e.g. opus-4.7/4.8/5, sonnet-5); 'adaptive_only' models accept nothing but 'adaptive' (e.g. fable, mythos). - #[serde(rename = "adaptive_thinking", skip_serializing_if = "Option::is_none")] - pub adaptive_thinking: Option, - /// Whether this model supports reasoning effort configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub reasoning_effort: Option, - /// Whether this model supports canonical tool calling - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_calls: Option, - /// Whether this model supports vision/image input - #[serde(skip_serializing_if = "Option::is_none")] - pub vision: Option, -} - -/// Model capabilities and limits -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ModelCapabilities { - /// Token limits for prompts, outputs, and context window - #[serde(skip_serializing_if = "Option::is_none")] - pub limits: Option, - /// Feature flags indicating what the model supports - #[serde(skip_serializing_if = "Option::is_none")] - pub supports: Option, -} - /// A service-published message about a model, carrying a stable machine-readable code alongside human-readable text. /// ///
@@ -13966,6 +15314,9 @@ pub struct Model { /// Supported reasoning effort levels (only present if model supports reasoning effort) #[serde(skip_serializing_if = "Option::is_none")] pub supported_reasoning_efforts: Option>, + /// Model vendor as the Copilot API reports it, for example "Anthropic" or "Azure OpenAI". Open vocabulary, passed through unchanged. It can name the vendor that serves the model instead of the one that built it, or a label that is not a vendor, such as "Experimental". Absent when the Copilot API reports no vendor. + #[serde(skip_serializing_if = "Option::is_none")] + pub vendor: Option, /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub warning_messages: Option>, @@ -14194,6 +15545,235 @@ pub struct ModelPickerPersistenceRequest { pub settings_context: ModelPickerSettingsContext, } +/// Adapter-declared policy that tells clients whether discovery may run automatically. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAutomaticDiscoveryPolicy { + /// Whether automatic discovery is allowed, limited to configured providers, or explicit-only. + pub mode: ModelProviderAutomaticDiscoveryMode, + /// Maximum network scope used by this adapter during discovery. + pub network_scope: ModelProviderDiscoveryNetworkScope, + /// True when discovery requires non-null caller input. Omission or null is rejected before adapter execution. When false, omitted or null input selects adapter defaults without schema validation. + pub requires_input: bool, + /// True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. + pub requires_trust: bool, +} + +/// An operation supported by a model-provider adapter. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAdapterOperationDescriptor { + /// Optional self-contained JSON Schema Draft 7 for non-null discovery input. Only supported on discover. No external references are resolved. Omitted or null input selects defaults when requiresInput is false. Without a schema, the adapter validates supplied input. + #[serde(skip_serializing_if = "Option::is_none")] + pub input_schema: Option, + /// Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. + pub name: String, +} + +/// Contributor attribution, independent of routing identity and authorization. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAttribution { + /// Human-readable contributor name, not the adapter display name. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_display_name: Option, + /// Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_id: Option, + /// Kind of component that supplied the adapter. Attribution does not confer authority. + pub source: ModelProviderProvenanceSource, +} + +/// A normalized model-provider adapter in the session's effective catalog. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAdapterDescriptor { + /// Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. + pub adapter_id: String, + /// Adapter-declared policy for passive and automatic discovery. + pub automatic_discovery: ModelProviderAutomaticDiscoveryPolicy, + /// Human-readable provider name. + pub display_name: String, + /// Operations supported by this provider adapter. + pub operations: Vec, + /// Attribution for the adapter itself. + pub provenance: ModelProviderAttribution, + /// Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. + pub provider_kind: String, +} + +/// Normalized model-provider adapter definitions available to the session, not discovered instances. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderAdapterCatalog { + /// Available provider adapters ordered by adapterId. + pub providers: Vec, +} + +/// A BYOK model definition referencing a named provider. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderModelConfig { + /// Optional capability overrides (vision, tool_calls, reasoning, etc.). + #[serde(skip_serializing_if = "Option::is_none")] + pub capabilities: Option, + /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. + pub id: String, + /// Maximum context window tokens for the model. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_context_window_tokens: Option, + /// Maximum output tokens for the model. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_output_tokens: Option, + /// Maximum prompt/input tokens for the model. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_prompt_tokens: Option, + /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, + /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_id: Option, + /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Name of the configured provider that serves this model. + pub provider: String, + /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. + #[serde(skip_serializing_if = "Option::is_none")] + pub system_message: Option, + /// The model name sent to the provider API for inference. Defaults to `id`. + #[serde(skip_serializing_if = "Option::is_none")] + pub wire_model: Option, +} + +/// Azure-specific provider options. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderConfigAzure { + /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + #[serde(skip_serializing_if = "Option::is_none")] + pub api_version: Option, +} + +/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NamedProviderConfig { + /// Static API key used to authenticate provider requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, + /// Azure authentication configuration for the provider. + #[serde(skip_serializing_if = "Option::is_none")] + pub azure: Option, + /// Base URL for provider API requests. + pub base_url: String, + /// Static bearer token used to authenticate provider requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub bearer_token: Option, + /// Whether the host supplies bearer tokens dynamically. + #[serde(skip_serializing_if = "Option::is_none")] + pub has_bearer_token_provider: Option, + /// Additional HTTP headers included with provider requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub headers: Option>, + /// The product serving the provider's models, reported in telemetry as `model_provider`. Only affects telemetry. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_provider: Option, + /// Unique provider name used to qualify model selection IDs. + pub name: String, + /// Transport used to communicate with the provider. + #[serde(skip_serializing_if = "Option::is_none")] + pub transport: Option, + /// Provider protocol family. + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, + /// Wire API used to communicate with the provider. + #[serde(skip_serializing_if = "Option::is_none")] + pub wire_api: Option, +} + +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderConfigurationPlan { + /// Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. + pub model: ProviderModelConfig, + /// Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. + pub model_disposition: ModelProviderConfigurationDisposition, + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + pub provider: NamedProviderConfig, + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + pub provider_disposition: ModelProviderConfigurationDisposition, + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + pub selection_id: String, + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + pub warnings: Vec, +} + /// One model provider available to the session — the model analog of the account `ProviderDescriptor`. Opaque id/label/kind plus a stable ordering; central code never branches on kind. /// ///
@@ -14215,6 +15795,162 @@ pub struct ModelProviderDescriptor { pub ordering: i64, } +/// Provider discovery parameters. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderDiscoverRequest { + /// Opaque adapter identity returned by `session.providers.getCatalog`. + pub adapter_id: String, + /// Provider-specific JSON input. Omission or null selects adapter defaults unless requiresInput is true. Non-null input is validated against the advertised Draft 7 schema when present; otherwise validation belongs to the adapter. + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, +} + +/// Serializable reference to a discovered provider instance. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderInstanceReference { + /// Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. + pub adapter_id: String, + /// Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + pub id: String, + /// Absolute provider management URI. The adapter validates normalization, supported schemes, and permission to access it against its bound configuration; a reference does not grant authority. + pub management_endpoint: String, + /// Descriptive provider family. Must match the selected adapter; not a routing key. + pub provider_kind: String, +} + +/// A normalized model-provider instance discovered by the runtime. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderInstance { + /// Human-readable instance name. + pub display_name: String, + /// Inference API endpoint when the provider exposes one separately from its management endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_endpoint: Option, + /// Transport to use for inference against this instance. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_transport: Option, + /// Provider family to use for inference against this instance. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_type: Option, + /// Wire API to use for inference against this instance, when required by the provider family. + #[serde(skip_serializing_if = "Option::is_none")] + pub inference_wire_api: Option, + /// Attribution for the adapter that produced this instance. + pub provenance: ModelProviderProvenance, + /// Self-contained reference for subsequent provider operations. + pub reference: ModelProviderInstanceReference, +} + +/// Provider instances found by a discovery operation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderDiscoverResult { + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + pub instances: Vec, + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + pub outcome: ModelProviderOperationOutcome, +} + +/// Provider status request parameters. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderGetStatusRequest { + /// Provider instance reference returned by discovery. + pub instance: ModelProviderInstanceReference, +} + +/// Provider model inventory request parameters. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderModelsListRequest { + /// Provider instance reference returned by discovery. + pub instance: ModelProviderInstanceReference, +} + +/// A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderPrepareConfigurationRequest { + /// The discovered instance that serves the model. + pub instance: ModelProviderInstance, + /// The discovered model to configure. + pub model: DiscoveredModel, +} + +/// Current health information for a provider instance. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelProviderStatus { + /// Normalized provider instance. + pub instance: ModelProviderInstance, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + pub status: String, + /// Provider-reported version. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + /// Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. /// ///
@@ -14569,63 +16305,6 @@ pub struct MoveMcpLoadingToBackgroundResult { pub moved_to_background: bool, } -/// Azure-specific provider options. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProviderConfigAzure { - /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - #[serde(skip_serializing_if = "Option::is_none")] - pub api_version: Option, -} - -/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NamedProviderConfig { - /// Static API key used to authenticate provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub api_key: Option, - /// Azure authentication configuration for the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub azure: Option, - /// Base URL for provider API requests. - pub base_url: String, - /// Static bearer token used to authenticate provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub bearer_token: Option, - /// Whether the host supplies bearer tokens dynamically. - #[serde(skip_serializing_if = "Option::is_none")] - pub has_bearer_token_provider: Option, - /// Additional HTTP headers included with provider requests. - #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, - /// Unique provider name used to qualify model selection IDs. - pub name: String, - /// Transport used to communicate with the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub transport: Option, - /// Provider protocol family. - #[serde(skip_serializing_if = "Option::is_none")] - pub r#type: Option, - /// Wire API used to communicate with the provider. - #[serde(skip_serializing_if = "Option::is_none")] - pub wire_api: Option, -} - /// The session's friendly name, or null when not yet set. /// ///
@@ -17033,50 +18712,6 @@ pub struct ProtocolSystemMessageReplaceConfig { pub mode: ProtocolReplaceMode, } -/// A BYOK model definition referencing a named provider. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProviderModelConfig { - /// Optional capability overrides (vision, tool_calls, reasoning, etc.). - #[serde(skip_serializing_if = "Option::is_none")] - pub capabilities: Option, - /// Provider-local model id, unique within its provider. The session-wide selection id (shown in the model list and passed to switchTo) is the provider-qualified `provider/id`. - pub id: String, - /// Maximum context window tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_context_window_tokens: Option, - /// Maximum output tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_output_tokens: Option, - /// Maximum prompt/input tokens for the model. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_prompt_tokens: Option, - /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option>, - /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - #[serde(skip_serializing_if = "Option::is_none")] - pub model_id: Option, - /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - /// Name of the configured provider that serves this model. - pub provider: String, - /// System-message configuration used when the runtime builds the standard prompt for this provider-qualified model, including general-purpose subagents. It uses the same object hierarchy as session-level systemMessage configuration, except transform actions are rejected because the current callback protocol is not model-scoped. When present, it overrides the session-wide configuration on those prompt paths. Selected custom-agent and specialized-subagent prompts remain authoritative. - #[serde(skip_serializing_if = "Option::is_none")] - pub system_message: Option, - /// The model name sent to the provider API for inference. Defaults to `id`. - #[serde(skip_serializing_if = "Option::is_none")] - pub wire_model: Option, -} - /// BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. /// ///
@@ -17154,6 +18789,9 @@ pub struct ProviderConfig { /// Well-known model ID used for capability lookup. When set, agent behavior config and token limits are inferred from this model. #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// The product serving the model, reported in telemetry as `model_provider`. Set it when `type` alone cannot identify the product, such as Ollama or LM Studio behind an OpenAI-compatible endpoint. Only affects telemetry. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// Provider name used for model and telemetry attribution. #[serde(skip_serializing_if = "Option::is_none")] pub provider_name: Option, @@ -19129,6 +20767,57 @@ pub struct SandboxHostSupport { pub supported: bool, } +/// Result of creating the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaCreateResult { + /// Absolute path of the public certificate of the certificate authority, in PEM format. + pub certificate_path: String, +} + +/// Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRequest { + /// The sandbox configuration that the host gives its sessions. The runtime reads the credential hosts from `auth` and `credentials`; it ignores `enabled` and the other fields. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_config: Option, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaStatus { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + /// Register an absolute-time scheduled prompt. /// ///
@@ -19698,35 +21387,6 @@ pub struct SessionActivity { pub has_active_work: bool, } -/// Current authentication information, or null when no authentication is active. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionAuthInfoResult { - /// Snapshot of the authenticated user's Copilot subscription info, if known - #[serde(skip_serializing_if = "Option::is_none")] - pub copilot_user: Option, - /// Name of the environment variable that supplied the credential, when applicable - #[serde(skip_serializing_if = "Option::is_none")] - pub env_var: Option, - /// Authentication host - pub host: String, - /// Authenticated login, when available - #[serde(skip_serializing_if = "Option::is_none")] - pub login: Option, - /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. - #[serde(skip_serializing_if = "Option::is_none")] - pub registration_id: Option, - /// Authentication type - pub r#type: AuthInfoType, -} - /// Internal GitHub login parameters. /// ///
@@ -19827,11 +21487,28 @@ pub struct SessionBulkDeleteResult { pub freed_bytes: HashMap, } +/// The IDE a host is connected to, as reported to the session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectedIdeInfo { + /// Display name of the connected IDE, for example `VS Code`. + pub ide_name: String, + /// Absolute path of the workspace folder the IDE has open. + pub workspace_folder: String, +} + /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -19885,7 +21562,7 @@ pub struct SessionContextAttributionEntriesItem { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: SessionContextAttributionCategories, @@ -19895,13 +21572,13 @@ pub struct SessionContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -19918,19 +21595,19 @@ pub struct SessionContextAttribution { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, /// Tokens consumed by the system prompt pub system_tokens: i64, @@ -19993,6 +21670,9 @@ pub struct SessionFsError { /// Free-form detail about the error, for logging/diagnostics #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + #[serde(skip_serializing_if = "Option::is_none")] + pub write_changed: Option, } /// Path to test for existence in the client-provided session filesystem. @@ -20137,6 +21817,41 @@ pub struct SessionFsReaddirWithTypesResult { pub error: Option, } +/// Path of the binary file to read from the client-provided session filesystem. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsReadFileBytesRequest { + /// Target session identifier + pub session_id: SessionId, + /// Path using SessionFs conventions + pub path: String, +} + +/// File bytes as standard base64, or a filesystem error if the read failed. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsReadFileBytesResult { + /// Exact file bytes encoded as standard base64 + pub content: String, + /// Describes a filesystem error. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + /// Path of the file to read from the client-provided session filesystem. /// ///
@@ -20225,6 +21940,9 @@ pub struct SessionFsRmRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFsSetProviderCapabilities { + /// Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes + #[serde(skip_serializing_if = "Option::is_none")] + pub binary: Option, /// Whether the provider supports SQLite query/exists operations #[serde(skip_serializing_if = "Option::is_none")] pub sqlite: Option, @@ -20444,6 +22162,28 @@ pub struct SessionFsStatResult { pub size: i64, } +/// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFsWriteFileBytesRequest { + /// Target session identifier + pub session_id: SessionId, + /// Path using SessionFs conventions + pub path: String, + /// Exact file bytes encoded as standard base64 + pub content: String, + /// Optional POSIX-style mode for newly created files + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + /// File path, content to write, and optional mode for the client-provided session filesystem. /// ///
@@ -20755,6 +22495,9 @@ pub struct SessionManagedPermissions { /// When set to `disable`, prevents bypass/allow-all permission modes. Advisory auto-approval remains available because normal prompt paths stay active. 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. #[serde(skip_serializing_if = "Option::is_none")] pub disable_bypass_permissions_mode: Option, + /// Closed-world host boundary expressed as `Domain(hostname)`, `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, queries, and fragments are rejected because every network request must be enforceable at host-level egress. Multiple managed sources intersect their lists; an empty list denies all hosts. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit_to: Option>, } /// Managed settings an SDK host may inject at session startup. Only permissions are accepted in this initial contract. @@ -20773,6 +22516,22 @@ pub struct SessionManagedSettings { pub permissions: Option, } +/// Records which IDE the host is connected to, or clears it. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpSetConnectedIdeInfoParams { + /// The connected IDE. Null or omitted clears the recorded IDE, which is how a host reports that it is disconnected. + #[serde(skip_serializing_if = "Option::is_none")] + pub ide: Option, +} + /// Public-facing projection of workspace metadata for SDK / TUI consumers #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -21715,6 +23474,85 @@ pub struct SessionsCloseRequest { #[serde(rename_all = "camelCase")] pub struct SessionsCloseResult {} +/// A working-directory context together with the client that produced it. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionWorkingDirectoryContextWithClient { + /// Merge-base commit SHA + #[serde(skip_serializing_if = "Option::is_none")] + pub base_commit: Option, + /// Current git branch name + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, + /// Name of the client that created the session + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + /// Current working directory path + pub cwd: String, + /// Root directory of the git repository + #[serde(skip_serializing_if = "Option::is_none")] + pub git_root: Option, + /// Head commit of the current git branch + #[serde(skip_serializing_if = "Option::is_none")] + pub head_commit: Option, + /// Hosting platform type of the repository + #[serde(skip_serializing_if = "Option::is_none")] + pub host_type: Option, + /// Repository identifier derived from the git remote URL + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Raw host string from the git remote URL + #[serde(skip_serializing_if = "Option::is_none")] + pub repository_host: Option, +} + +/// Identity, state location and starting context for a workspace record. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsCreateWorkspaceRequest { + /// Starting working-directory context. The record keeps `cwd`, `gitRoot`, `repository`, `hostType`, `branch`, and `clientName`. Other fields, including `repositoryHost`, `headCommit`, and `baseCommit`, are ignored. `hostType` must be `github` or `ado`. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// `windows` (any letter case) selects Windows path rules. Any other value selects POSIX path rules. + pub convention: String, + /// User-supplied display name for the workspace + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Session ID the workspace record belongs to + pub session_id: SessionId, + /// Directory the session's state is written under when no session filesystem provider is configured. Ignored when a provider is configured; the provider's session state path is used instead. + pub session_state_path: String, +} + +/// The workspace record that was written. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsCreateWorkspaceResult { + /// The created workspace record, as JSON + pub workspace_json: String, +} + /// Session ID to delete from disk. /// ///
@@ -22409,6 +24247,39 @@ pub struct SessionsLoadDeferredRepoHooksRequest { pub session_id: SessionId, } +/// Where the session's state lives, as a root directory and the session ID under it. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsLoadWorkspaceRequest { + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + pub session_id: SessionId, + /// Root directory every session's state directory sits under + pub sessions_home: String, +} + +/// The workspace record on disk, omitted when the session has none. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsLoadWorkspaceResult { + /// The workspace record, as JSON. Omitted when the record does not exist. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_json: Option, +} + /// Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). /// ///
@@ -22637,6 +24508,37 @@ pub struct SessionsTransferRemoteControlRequest { pub to_session_id: String, } +/// Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsUpdateWorkspaceFieldsRequest { + /// Workspace-schema fields to merge into the record, as a JSON object. Fields the object omits keep their stored values, except stored keys outside the schema are not preserved and a stored `fork_count` is never replaced. + pub fields_json: String, + /// Session ID naming the state directory under the sessions home. Rejected when it is absolute or contains a parent component, so it cannot escape the sessions home. + pub session_id: SessionId, + /// Root directory every session's state directory sits under + pub sessions_home: String, +} + +/// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsUpdateWorkspaceFieldsResult {} + /// Telemetry engagement ID for the session, when available. /// ///
@@ -22896,7 +24798,7 @@ pub struct ShellCancelUserRequestedRequest { pub request_id: RequestId, } -/// Shell command to run, with optional working directory and timeout in milliseconds. +/// Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. /// ///
/// @@ -22917,7 +24819,7 @@ pub struct ShellExecRequest { pub timeout: Option, } -/// Identifier of the spawned process, used to correlate streamed output and exit notifications. +/// Identifier of the spawned shell process, usable with shell.kill while the process is running. /// ///
/// @@ -22928,7 +24830,7 @@ pub struct ShellExecRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShellExecResult { - /// Unique identifier for tracking streamed output + /// Identifier usable with shell.kill while the process is running pub process_id: String, } @@ -23505,7 +25407,7 @@ pub struct SkillPlanUninstallRequest { pub policy_session_id: String, } -/// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. +/// Authoritative catalog metadata for one SDK-provided skill. The skill's SKILL.md text is fetched separately and lazily. /// ///
/// @@ -23563,7 +25465,7 @@ pub(crate) struct SkillProviderReadRequest { pub name: String, } -/// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. +/// Text-only SKILL.md content returned by an SDK session's skill provider. YAML frontmatter is optional: fields it omits come from the catalog descriptor, fields it declares must match the descriptor, and `allowed-tools` is read only from frontmatter. Related files and assets are not supported. /// ///
/// @@ -23574,8 +25476,8 @@ pub(crate) struct SkillProviderReadRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SkillProviderReadResult { - /// Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. - pub markdown: String, + /// SKILL.md text, with or without YAML frontmatter, or null when the provider has no skill with the requested name. The runtime enforces a 1 MiB UTF-8 byte limit. + pub markdown: Option, } /// Skill names to mark as disabled in global configuration, replacing any previous list. @@ -26138,7 +28040,7 @@ pub struct UserSettingMetadata { pub value: serde_json::Value, } -/// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. +/// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. /// ///
/// @@ -26168,21 +28070,6 @@ pub struct UserSettingsSetRequest { pub settings: serde_json::Value, } -/// Outcome of writing user settings. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserSettingsSetResult { - /// Top-level keys whose write landed in settings.json but is shadowed by a value still present in the legacy config.json (config.json wins on read). The write does not take effect until the legacy value is removed. - pub shadowed_keys: Vec, -} - /// Current sharing status and shareable GitHub URL for a session. /// ///
@@ -27935,6 +29822,86 @@ pub struct SandboxGetHostSupportResult { pub supported: bool, } +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaGetStatusResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRotateResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaTrustResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + +/// Status of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SandboxProxyCaRemoveResult { + /// Whether this process can add the certificate authority to OS trust without credentials from a different user. False where OS trust is unsupported, and on Windows when the process cannot elevate itself to write the machine trust store. When false, do not offer to set up the certificate authority. + pub can_install: bool, + /// Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The state of the certificate authority. + pub state: SandboxProxyCaState, +} + /// Built-in tools available for the requested model, with their parameters and instructions. /// ///
@@ -28230,6 +30197,81 @@ pub struct InstructionsGetDiscoveryPathsResult { pub paths: Vec, } +/// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalStateLoadForConfigDirResult { + /// Whether the user has answered the prompt suggesting they install the desktop app. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_install_nudge_responded: Option, + /// Whether the app tip has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub app_tip_shown: Option, + /// Terminals the user has already been asked to set up, so the host does not ask twice. + #[serde(skip_serializing_if = "Option::is_none")] + pub asked_setup_terminals: Option>, + /// When the Auto-feedback hint was last shown, as an ISO 8601 timestamp. It enforces the once-per-day cap for non-staff users across restarts. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_feedback_last_prompted_at: Option, + /// When the host first ran on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub first_launch_at: Option, + /// Plugins installed on this machine. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed_plugins: Option>, + /// Account used for the most recent sign-in. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) last_logged_in_user: Option, + /// Every account the host has signed in to on this machine. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) logged_in_users: Option>, + /// Whether the one-off cleanup of stored reasoning summaries has run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summaries_cleanup_done: Option, + /// Models the user selected recently, most recent first. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_model_ids: Option>, + /// Whether the user declined to trust the sandbox credential proxy CA. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_credential_proxy_ca_declined: Option, + /// Whether the sandbox onboarding has been shown. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_onboarding_shown: Option, + /// Whether the user is a GitHub or Microsoft staff member, which unlocks internal-only behavior. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff: Option, + /// Whether the user was recognized as GitHub staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_github: Option, + /// When the staff-only log level migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_log_level_migration_at: Option, + /// Whether the user was recognized as Microsoft staff. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_microsoft: Option, + /// When the staff-only model reset last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_model_reset_at: Option, + /// When the staff-only update channel migration last ran. + #[serde(skip_serializing_if = "Option::is_none")] + pub staff_update_channel_migration_at: Option, + /// Folders where the user declined the init prompt, so it stays hidden there. + #[serde(skip_serializing_if = "Option::is_none")] + pub suppress_init_folders: Option>, + /// Folders the user has marked as trusted. + #[serde(skip_serializing_if = "Option::is_none")] + pub trusted_folders: Option>, +} + /// Slash commands available in the session, after applying any include/exclude filters. /// ///
@@ -28245,6 +30287,57 @@ pub struct CommandsListResult { pub commands: Vec, } +/// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GitHubOwnersNextRequestIdResult { + /// Request id to pass to `gitHubOwners.list` and, to abandon it, `gitHubOwners.cancel`. + pub request_id: i64, +} + +/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitWorkingDirectoryContextResult { + /// Merge-base commit SHA (fork point from the remote default branch) + #[serde(skip_serializing_if = "Option::is_none")] + pub base_commit: Option, + /// Current git branch name + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, + /// Current working directory path + pub cwd: String, + /// Root directory of the git repository, resolved via git rev-parse + #[serde(skip_serializing_if = "Option::is_none")] + pub git_root: Option, + /// Head commit of the current git branch + #[serde(skip_serializing_if = "Option::is_none")] + pub head_commit: Option, + /// Hosting platform type of the repository + #[serde(skip_serializing_if = "Option::is_none")] + pub host_type: Option, + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com") + #[serde(skip_serializing_if = "Option::is_none")] + pub repository_host: Option, +} + /// Result of opening a session. /// ///
@@ -28512,6 +30605,197 @@ pub struct SessionsGetRemoteControlStatusResult { pub status: serde_json::Value, } +/// Feature availability. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsGetCapabilitiesResult { + /// API version. + pub api_version: i64, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, + /// Whether results are cached. + pub conditional_cache: bool, + /// Whether accounts are selected by opaque ID. + pub opaque_account_selection: bool, +} + +/// Eligible accounts. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsGetAccountsResult { + /// Eligible accounts. + pub accounts: Vec, + /// Availability. + pub availability: ConnectorDiscoveryAvailability, +} + +/// Entries for the selected account. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsListResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + +/// Entries for the selected account. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorsRefreshResult { + /// Opaque account ID. + pub account_id: String, + /// Entries. + pub connectors: Vec, + /// Refresh time in Unix epoch milliseconds. + pub refreshed_at_ms: i64, + /// Revision. + pub revision: i64, +} + +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetCatalogParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Normalized model-provider adapter definitions available to the session, not discovered instances. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetCatalogResult { + /// Available provider adapters ordered by adapterId. + pub providers: Vec, +} + +/// Provider instances found by a discovery operation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersDiscoverResult { + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + pub instances: Vec, + /// Typed operation outcome. Passive discovery can return `absent` with an empty instance list. + pub outcome: ModelProviderOperationOutcome, +} + +/// Current health information for a provider instance. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersGetStatusResult { + /// Normalized provider instance. + pub instance: ModelProviderInstance, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + pub status: String, + /// Provider-reported version. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +/// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersModelsListResult { + /// Provider-native models in provider order. + pub models: Vec, + /// Typed operation outcome. + pub outcome: ModelProviderOperationOutcome, +} + +/// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProvidersModelsPrepareConfigurationResult { + /// Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. + pub model: ProviderModelConfig, + /// Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. + pub model_disposition: ModelProviderConfigurationDisposition, + /// Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. + pub provider: NamedProviderConfig, + /// Whether `provider` still needs to be registered. When `alreadyConfigured`, a provider with the same endpoint is already registered and `provider` restates it under its existing name; adding it again is rejected as a duplicate. + pub provider_disposition: ModelProviderConfigurationDisposition, + /// Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + pub selection_id: String, + /// Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + pub warnings: Vec, +} + /// Identifies the target session. /// ///
@@ -29428,6 +31712,9 @@ pub struct SessionModelGetCurrentResult { /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. #[serde(skip_serializing_if = "Option::is_none")] pub pending_auto_tier: Option, + /// Captured base model to restore when leaving plan mode. Omitted outside plan mode or when no plan override has captured a base model. Persistent agent model requirements apply to this model rather than the temporary plan model. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_base_model_id: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -30563,23 +32850,6 @@ pub struct SessionCustomizationsReloadParams { pub session_id: SessionId, } -/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionCustomizationsReloadResult { - /// Errors emitted while loading skills (e.g. skills that failed to load entirely) - pub errors: Vec, - /// Warnings emitted while loading skills (e.g. skills that loaded but had issues) - pub warnings: Vec, -} - /// Indicates whether fleet mode was successfully activated. /// ///
@@ -31117,6 +33387,36 @@ pub struct SessionMcpListResult { pub servers: Vec, } +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Effective MCP configuration with optional live observations from matching already materialized servers. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpListConfiguredResult { + /// Effective configured MCP servers. + pub servers: Vec, +} + /// Tools exposed by the connected MCP server. Throws when the server is not connected. /// ///
@@ -31378,7 +33678,10 @@ pub struct SessionMcpOauthPrepareLoginResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMcpOauthLoginResult { - /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. When present, the runtime starts the callback listener before returning and continues the flow in the background; completion is signaled via session.mcp_server_status_changed. + /// Opaque authorization identifier returned only for a host-managed redirect URI. The runtime also sends it as the OAuth state value, so the callback endpoint can read state and pass it with the full callback URL to session.mcp.oauth.complete. + #[serde(skip_serializing_if = "Option::is_none")] + pub authorization_id: Option, + /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. For the default loopback flow, the runtime starts its listener before returning. With redirectUri, the host receives the callback and completes it through session.mcp.oauth.complete. The runtime continues the flow in the background and signals completion via session.mcp_server_status_changed. #[serde(skip_serializing_if = "Option::is_none")] pub authorization_url: Option, /// Runtime-issued owned flow identity; never a server name or installation operation ID. @@ -31579,6 +33882,54 @@ pub struct SessionMcpResourcesListTemplatesResult { pub resource_templates: Vec, } +/// One page of prompts advertised by the named MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpPromptsListResult { + /// MCP result metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard result fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Opaque cursor for the next page, if the server has more prompts + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Prompts advertised by the server + pub prompts: Vec, +} + +/// Prompt messages returned by the MCP server without sending them to the model. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpPromptsGetResult { + /// MCP result metadata + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, + /// Server-provided non-standard result fields + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_properties: Option>, + /// Description of the prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Ordered prompt messages + pub messages: Vec, +} + /// Per-source session diagnostics configuration. /// ///
@@ -31682,6 +34033,27 @@ pub struct SessionConnectorsGetCapabilitiesResult { pub max_poll_interval_ms: i64, /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. pub opaque_account_selection: bool, + /// Whether getAccount is supported. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_account_selection: Option, + /// Whether reconcile accepts forceConnectorName. Absence means false. + #[serde(skip_serializing_if = "Option::is_none")] + pub targeted_reconcile: Option, +} + +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsGetAccountParams { + /// Target session identifier + pub session_id: SessionId, } /// Identifies the target session. @@ -32566,6 +34938,21 @@ pub struct SessionUiHandlePendingElicitationResult { pub success: bool, } +/// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanAskUserResult { + /// Whether the response was accepted. False if the request was already resolved by another client. + pub success: bool, +} + /// Indicates whether the pending UI request was resolved by this call. /// ///
@@ -32581,6 +34968,21 @@ pub struct SessionUiHandlePendingUserInputResult { pub success: bool, } +/// Indicates whether the pending UI request was resolved by this call. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanUserInputResult { + /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. + pub success: bool, +} + /// Indicates whether the pending UI request was resolved by this call. /// ///
@@ -32641,6 +35043,21 @@ pub struct SessionUiHandlePendingExitPlanModeResult { pub success: bool, } +/// Indicates whether the pending UI request was resolved by this call. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionUiHandleHumanExitPlanModeResult { + /// True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. + pub success: bool, +} + /// Identifies the target session. /// ///
@@ -33217,19 +35634,19 @@ pub struct SessionMetadataActivityResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataContextInfoResultContextInfo { - /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) + /// Output reservation overlapping the displayed prompt allowance plus tokens after the effective input budget's buffer-exhaustion blocking threshold (default 95%). pub buffer_tokens: i64, /// Token count at which background compaction starts (configurable percentage of promptTokenLimit) pub compaction_threshold: i64, /// Tokens consumed by user/assistant/tool messages pub conversation_tokens: i64, - /// Prompt token limit plus the model's full output token limit. + /// Advertised prompt allowance for the selected context tier, without adding output tokens. The denominator for context-usage displays. pub limit: i64, /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools) pub mcp_tools_tokens: i64, /// The model used for token counting pub model_name: String, - /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) + /// Effective input budget: the selected tier's prompt allowance bounded by the combined context ceiling minus the requested output allowance. Uses DEFAULT_TOKEN_LIMIT when limits are unspecified. pub prompt_token_limit: i64, /// Tokens consumed by the system prompt pub system_tokens: i64, @@ -33273,7 +35690,7 @@ pub struct SessionMetadataGetContextAttributionParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataGetContextAttributionResultContextAttributionCategories { - /// Output reserve plus post-blocking-threshold buffer. + /// Overlapping output reservation plus post-blocking-threshold buffer. pub buffer: i64, /// Custom-instructions tokens (0 when none are configured). pub custom_instructions: i64, @@ -33320,7 +35737,7 @@ pub struct SessionMetadataGetContextAttributionResultContextAttributionEntriesIt #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMetadataGetContextAttributionResultContextAttribution { - /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + /// Output reservation overlapping the displayed prompt allowance plus the tokens past the effective input budget's buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. pub buffer_tokens: i64, /// The six normalized `/context` header buckets, computed from the same tokenization as `entries` so the two never disagree. Convenience rollups: `freeSpace` and `buffer` describe window capacity rather than occupied context, so the values do not sum to `totalTokens`. pub categories: SessionMetadataGetContextAttributionResultContextAttributionCategories, @@ -33330,13 +35747,13 @@ pub struct SessionMetadataGetContextAttributionResultContextAttribution { pub compaction_threshold: i64, /// Flat list of per-source attribution entries. Group by `kind` and render unrecognized kinds generically. Nesting and rollups are expressed via `parentId`. pub entries: Vec, - /// Prompt limit plus the model's output reserve: the full context window `categories.freeSpace` and `categories.buffer` are measured against. Mirrors `SessionContextInfo.limit`. + /// Advertised prompt allowance for the selected context tier: the denominator for context-usage displays and capacity for `categories.freeSpace` and `categories.buffer`. Mirrors `SessionContextInfo.limit`. pub limit: i64, /// The concrete model id the entire breakdown was tokenized against (feeds the per-model token multiplier). Under `Auto` (Free/Student) this is the resolved model, not the literal `auto` sentinel, so totals are not undercounted. A single-model approximation of a potentially multi-model Auto session. pub model_id: String, /// How `modelId` was chosen. Not a closed set — tolerate unknown values. Known values today: `autoResolved` (the model Auto resolved to), `selected` (the user's explicitly selected model), `default` (a fallback before any model is known). pub model_source: String, - /// Maximum prompt tokens the resolved model accepts — the denominator for a `##k/###k` context-usage display. Mirrors `SessionContextInfo.promptTokenLimit`. + /// Effective input budget after reserving requested output against the combined context ceiling. Mirrors `SessionContextInfo.promptTokenLimit`. pub prompt_token_limit: i64, /// Total token count of the current context window the entries are measured against (system message + conversation messages + tool definitions — the same total reported by /context). Divide an entry's `tokens` by this to derive its share. pub total_tokens: i64, @@ -33487,7 +35904,7 @@ pub struct SessionContentExclusionCheckPathsResult { pub checks: Vec, } -/// Identifier of the spawned process, used to correlate streamed output and exit notifications. +/// Identifier of the spawned shell process, usable with shell.kill while the process is running. /// ///
/// @@ -33498,7 +35915,7 @@ pub struct SessionContentExclusionCheckPathsResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionShellExecResult { - /// Unique identifier for tracking streamed output + /// Identifier usable with shell.kill while the process is running pub process_id: String, } @@ -34722,6 +37139,16 @@ pub type CatalogResourceVersion = String; ///
pub type ClientMetadata = HashMap; +/// Session account selection, or null. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+pub type ConnectorSessionAccountResult = Option; + /// HTTP headers as a map from lowercased header name to a list of values. Multi-valued headers (e.g. Set-Cookie) preserve all values. /// ///
@@ -34762,6 +37189,16 @@ pub type McpPlanSecretReference = String; ///
pub type SandboxHostCapabilityName = String; +/// Current authentication information, or null when no authentication is active. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+pub type SessionAuthInfoResult = Option; + /// Ordered client metadata outcomes for the requested local sessions. /// ///
@@ -34951,6 +37388,14 @@ pub enum UserAuthInfoType { User, } +/// Provider-owned account authentication. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AccountAuthInfoType { + #[serde(rename = "account")] + #[default] + Account, +} + /// Authentication via the `gh` CLI's saved credentials. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum GhCliAuthInfoType { @@ -34984,6 +37429,7 @@ pub enum AuthInfo { TokenProvider(TokenProviderAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } @@ -35690,6 +38136,9 @@ pub enum AuthInfoType { /// Authentication from an interactive user sign-in. #[serde(rename = "user")] User, + /// Authentication from a selected provider-owned account, without a GitHub credential. + #[serde(rename = "account")] + Account, /// Authentication delegated to the GitHub CLI. #[serde(rename = "gh-cli")] GhCli, @@ -35751,7 +38200,7 @@ pub enum AuthLoginStepCompletedKind { Completed, } -/// Terminal disposition of a login persistence attempt. +/// Disposition of a login attempt, including pending user decisions. /// ///
/// @@ -35761,12 +38210,15 @@ pub enum AuthLoginStepCompletedKind { ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AuthLoginResultStatus { - /// The credential was persisted and the account is signed in. + /// The credential was persisted and the selected account is signed in. #[serde(rename = "completed")] Completed, /// Persistence needs explicit consent to store the token in plaintext. #[serde(rename = "needs-plaintext-consent")] NeedsPlaintextConsent, + /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. + #[serde(rename = "needs-account-selection")] + NeedsAccountSelection, /// The user declined plaintext persistence. #[serde(rename = "declined")] Declined, @@ -37465,6 +39917,31 @@ pub enum ConnectorConnectResult { Pending(ConnectorConnectResultPending), } +/// Availability. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorDiscoveryAvailability { + /// Enabled. + #[serde(rename = "enabled")] + Enabled, + /// Disabled. + #[serde(rename = "disabled")] + Disabled, + /// Unavailable. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Closed set of public task kinds a connection can negotiate. /// ///
@@ -37515,6 +39992,124 @@ pub enum ContentFilterMode { Unknown, } +/// Result of reloading a customization component. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum CustomizationReloadStatus { + /// The component was refreshed successfully. + Reloaded, + /// The component was not configured, loaded, or eligible for refresh. + Skipped, + /// The component could not be refreshed; other components may still reload. + Failed, + /// Unknown variant for forward compatibility. + Unknown(String), +} + +impl Default for CustomizationReloadStatus { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} + +impl From for CustomizationReloadStatus { + fn from(value: String) -> Self { + match value.as_str() { + "reloaded" => Self::Reloaded, + "skipped" => Self::Skipped, + "failed" => Self::Failed, + _ => Self::Unknown(value), + } + } +} + +impl From for String { + fn from(value: CustomizationReloadStatus) -> Self { + match value { + CustomizationReloadStatus::Reloaded => "reloaded".to_owned(), + CustomizationReloadStatus::Skipped => "skipped".to_owned(), + CustomizationReloadStatus::Failed => "failed".to_owned(), + CustomizationReloadStatus::Unknown(value) => value, + } + } +} + +/// Component of session customization discovery. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum CustomizationReloadSubsystem { + /// Repository metadata and working-directory context. + RepositoryContext, + /// Session instructions and their cached dynamic context. + Instructions, + /// Discovered plugin configuration. + Plugins, + /// Configured session and plugin hooks. + Hooks, + /// Discovered skills. + Skills, + /// Discovered custom agents. + Agents, + /// Loaded MCP server configuration. + Mcp, + /// Configured session extensions. + Extensions, + /// Unknown variant for forward compatibility. + Unknown(String), +} + +impl Default for CustomizationReloadSubsystem { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} + +impl From for CustomizationReloadSubsystem { + fn from(value: String) -> Self { + match value.as_str() { + "repositoryContext" => Self::RepositoryContext, + "instructions" => Self::Instructions, + "plugins" => Self::Plugins, + "hooks" => Self::Hooks, + "skills" => Self::Skills, + "agents" => Self::Agents, + "mcp" => Self::Mcp, + "extensions" => Self::Extensions, + _ => Self::Unknown(value), + } + } +} + +impl From for String { + fn from(value: CustomizationReloadSubsystem) -> Self { + match value { + CustomizationReloadSubsystem::RepositoryContext => "repositoryContext".to_owned(), + CustomizationReloadSubsystem::Instructions => "instructions".to_owned(), + CustomizationReloadSubsystem::Plugins => "plugins".to_owned(), + CustomizationReloadSubsystem::Hooks => "hooks".to_owned(), + CustomizationReloadSubsystem::Skills => "skills".to_owned(), + CustomizationReloadSubsystem::Agents => "agents".to_owned(), + CustomizationReloadSubsystem::Mcp => "mcp".to_owned(), + CustomizationReloadSubsystem::Extensions => "extensions".to_owned(), + CustomizationReloadSubsystem::Unknown(value) => value, + } + } +} + /// Source category for a collected debug bundle entry. /// ///
@@ -37984,7 +40579,7 @@ pub enum DiscoveredMcpServerType { Unknown, } -/// How far OneAuth may go to acquire the requested token. +/// Kind of component that supplied a provider adapter or row. Attribution does not confer authority. /// ///
/// @@ -37993,39 +40588,26 @@ pub enum DiscoveredMcpServerType { /// ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntraTokenInteraction { - /// Acquire the token without any user interaction, failing if interaction would be required. - #[serde(rename = "silent")] - Silent, - /// Allow interactive acquisition, prompting the user only when a cached or silent token is unavailable. - #[serde(rename = "interactive")] - Interactive, - /// Always prompt interactively, bypassing any cached or silently-refreshable token. - #[serde(rename = "force-interactive")] - ForceInteractive, +pub enum ModelProviderProvenanceSource { + /// Built into the runtime. + #[serde(rename = "builtIn")] + BuiltIn, + /// Derived from existing user configuration. + #[serde(rename = "configured")] + Configured, + /// Supplied by an extension. + #[serde(rename = "extension")] + Extension, + /// Supplied by another trusted contributor. + #[serde(rename = "custom")] + Custom, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// OneAuth token acquisition outcome discriminator. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntraTokenAcquireResultOkStatus { - #[serde(rename = "ok")] - #[default] - Ok, -} - -/// OneAuth token acquisition outcome discriminator. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntraTokenAcquireResultInteractionRequiredStatus { - #[serde(rename = "interaction-required")] - #[default] - InteractionRequired, -} - -/// Result of a OneAuth token acquisition. +/// Typed outcome for a provider operation. /// ///
/// @@ -38033,11 +40615,24 @@ pub enum EntraTokenAcquireResultInteractionRequiredStatus { /// and may change or be removed in future SDK or CLI releases. /// ///
-#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum EntraTokenAcquireResult { - Ok(EntraTokenAcquireResultOk), - InteractionRequired(EntraTokenAcquireResultInteractionRequired), +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelProviderOperationOutcomeCode { + /// The operation completed successfully; an empty inventory is valid. + #[serde(rename = "success")] + Success, + /// The provider or instance is absent during discovery, status, or model listing. Distinct from a successful empty inventory. + #[serde(rename = "absent")] + Absent, + /// The provider is configured or expected but could not be reached. + #[serde(rename = "unreachable")] + Unreachable, + /// The operation failed for a reason other than absence or reachability. + #[serde(rename = "failed")] + Failed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, } /// GitHub Mission Control compute kind. @@ -40200,6 +42795,28 @@ pub enum McpPlanInstallResult { Unavailable(CatalogUnavailableError), } +/// The sender role of an MCP prompt message. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpPromptRole { + /// A message from the user. + #[serde(rename = "user")] + User, + /// A message from the assistant. + #[serde(rename = "assistant")] + Assistant, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. /// ///
@@ -40522,7 +43139,7 @@ pub enum ModelProviderKind { Unknown, } -/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// When the runtime may run an adapter without an explicit user action. /// ///
/// @@ -40531,13 +43148,106 @@ pub enum ModelProviderKind { /// ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ModelSwitchAutoTierStatus { - /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. - #[serde(rename = "unchanged")] - Unchanged, - /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. - #[serde(rename = "pending")] - Pending, +pub enum ModelProviderAutomaticDiscoveryMode { + /// The adapter declares that automatic discovery is safe when the other policy fields are satisfied. + #[serde(rename = "automatic")] + Automatic, + /// The adapter may refresh instances the user already configured, but must not scan for new instances automatically. + #[serde(rename = "configuredOnly")] + ConfiguredOnly, + /// The adapter must run only after an explicit user action. + #[serde(rename = "explicit")] + Explicit, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Network reach an adapter may use during discovery. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelProviderDiscoveryNetworkScope { + /// Discovery does not contact a network service. + #[serde(rename = "none")] + None, + /// Discovery is limited to loopback addresses on the local machine. + #[serde(rename = "loopbackOnly")] + LoopbackOnly, + /// Discovery contacts only endpoints the user already configured. + #[serde(rename = "configuredEndpointOnly")] + ConfiguredEndpointOnly, + /// Discovery may scan or contact the local network. + #[serde(rename = "localNetwork")] + LocalNetwork, + /// Discovery may contact remote internet services. + #[serde(rename = "internet")] + Internet, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Whether a planned configuration entry is new or already present in the session registry. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelProviderConfigurationDisposition { + /// No matching entry is registered; the caller should add the entry. + #[serde(rename = "create")] + Create, + /// An equivalent entry is already registered; the caller should reuse it rather than adding a duplicate. + #[serde(rename = "alreadyConfigured")] + AlreadyConfigured, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// The product serving the model, reported in telemetry as `model_provider`. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderConfigModelProvider { + /// OpenAI API. + #[serde(rename = "openai")] + Openai, + /// Anthropic API. + #[serde(rename = "anthropic")] + Anthropic, + /// Azure OpenAI Service. + #[serde(rename = "azure_openai")] + AzureOpenai, + /// Ollama. + #[serde(rename = "ollama")] + Ollama, + /// LM Studio. + #[serde(rename = "lm_studio")] + LmStudio, + /// Foundry Local. + #[serde(rename = "foundry_local")] + FoundryLocal, + /// llama.cpp server. + #[serde(rename = "llama_cpp")] + LlamaCpp, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -40613,6 +43323,97 @@ pub enum ProviderConfigWireApi { Unknown, } +/// Transport to be used for provider requests. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointTransport { + /// HTTP request/streaming transport. + #[serde(rename = "http")] + Http, + /// WebSocket transport. + #[serde(rename = "websockets")] + Websockets, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Provider family. Matches the `type` field of a BYOK provider config. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointType { + /// OpenAI-compatible endpoint (use the OpenAI client library). + #[serde(rename = "openai")] + Openai, + /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). + #[serde(rename = "azure")] + Azure, + /// Anthropic endpoint (use the Anthropic client library). + #[serde(rename = "anthropic")] + Anthropic, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Wire API to be used, when required for the provider type. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProviderEndpointWireApi { + /// Classic chat-completions request shape. + #[serde(rename = "completions")] + Completions, + /// Newer responses request shape. + #[serde(rename = "responses")] + Responses, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelSwitchAutoTierStatus { + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + #[serde(rename = "unchanged")] + Unchanged, + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + #[serde(rename = "pending")] + Pending, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. /// ///
@@ -41557,75 +44358,6 @@ pub enum ProtocolStaticSectionAction { Unknown, } -/// Transport to be used for provider requests. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointTransport { - /// HTTP request/streaming transport. - #[serde(rename = "http")] - Http, - /// WebSocket transport. - #[serde(rename = "websockets")] - Websockets, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Provider family. Matches the `type` field of a BYOK provider config. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointType { - /// OpenAI-compatible endpoint (use the OpenAI client library). - #[serde(rename = "openai")] - Openai, - /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). - #[serde(rename = "azure")] - Azure, - /// Anthropic endpoint (use the Anthropic client library). - #[serde(rename = "anthropic")] - Anthropic, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Wire API to be used, when required for the provider type. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ProviderEndpointWireApi { - /// Classic chat-completions request shape. - #[serde(rename = "completions")] - Completions, - /// Newer responses request shape. - #[serde(rename = "responses")] - Responses, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Attachment type discriminator #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PushAttachmentBlobType { @@ -41979,6 +44711,34 @@ pub enum SandboxConfigSource { Unknown, } +/// State of the persistent certificate authority of the sandbox credential proxy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SandboxProxyCaState { + /// This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. + #[serde(rename = "unsupported")] + Unsupported, + /// OS trust does not include the certificate authority, or none is stored. + #[serde(rename = "notInstalled")] + NotInstalled, + /// OS trust includes the stored certificate authority. + #[serde(rename = "installed")] + Installed, + /// The runtime could not read the certificate authority or the OS trust store. + #[serde(rename = "error")] + Error, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// A session-scoped sandbox transition applied while handling a slash command /// ///
@@ -42676,6 +45436,7 @@ pub enum SettableAuthInfo { Token(SettableTokenAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index f6ac0d769e..98590e2ad9 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -29,13 +29,6 @@ impl<'a> ClientRpc<'a> { } } - /// `accounts.*` sub-namespace. - pub fn accounts(&self) -> ClientRpcAccounts<'a> { - ClientRpcAccounts { - client: self.client, - } - } - /// `agentRegistry.*` sub-namespace. pub fn agent_registry(&self) -> ClientRpcAgentRegistry<'a> { ClientRpcAgentRegistry { @@ -64,6 +57,13 @@ impl<'a> ClientRpc<'a> { } } + /// `connectors.*` sub-namespace. + pub fn connectors(&self) -> ClientRpcConnectors<'a> { + ClientRpcConnectors { + client: self.client, + } + } + /// `environments.*` sub-namespace. pub fn environments(&self) -> ClientRpcEnvironments<'a> { ClientRpcEnvironments { @@ -78,6 +78,34 @@ impl<'a> ClientRpc<'a> { } } + /// `git.*` sub-namespace. + pub fn git(&self) -> ClientRpcGit<'a> { + ClientRpcGit { + client: self.client, + } + } + + /// `gitHubOwners.*` sub-namespace. + pub fn git_hub_owners(&self) -> ClientRpcGitHubOwners<'a> { + ClientRpcGitHubOwners { + client: self.client, + } + } + + /// `gitHubRepository.*` sub-namespace. + pub fn git_hub_repository(&self) -> ClientRpcGitHubRepository<'a> { + ClientRpcGitHubRepository { + client: self.client, + } + } + + /// `globalState.*` sub-namespace. + pub fn global_state(&self) -> ClientRpcGlobalState<'a> { + ClientRpcGlobalState { + client: self.client, + } + } + /// `hooks.*` sub-namespace. pub fn hooks(&self) -> ClientRpcHooks<'a> { ClientRpcHooks { @@ -437,45 +465,6 @@ impl<'a> ClientRpcAccount<'a> { } } -/// `accounts.*` RPCs. -#[derive(Clone, Copy)] -pub struct ClientRpcAccounts<'a> { - pub(crate) client: &'a Client, -} - -impl<'a> ClientRpcAccounts<'a> { - /// Acquire a Microsoft Entra access token through the runtime's OneAuth broker. Account-scoped because it uses the same native broker as the account stack: a trusted host application mints a scoped Entra token for its own use, most notably to authenticate to a remote MCP server whose authorization server is Entra ID (in place of the generic browser-OAuth flow). - /// - /// Wire method: `accounts.acquireEntraToken`. - /// - /// # Parameters - /// - /// * `params` - OneAuth token request supplied by a trusted host application. - /// - /// # Returns - /// - /// Result of a OneAuth token acquisition. - /// - ///
- /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
- pub(crate) async fn acquire_entra_token( - &self, - params: EntraTokenAcquireRequest, - ) -> Result { - let wire_params = serde_json::to_value(params)?; - let _value = self - .client - .call(rpc_methods::ACCOUNTS_ACQUIREENTRATOKEN, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } -} - /// `agentRegistry.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcAgentRegistry<'a> { @@ -580,6 +569,160 @@ impl<'a> ClientRpcAgents<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Lists the agents this runtime ships, by name. A consumer separating shipped agents from ones the user or a plugin authored should compare against these names rather than against `AgentInfo.source`: an authored agent may carry the `builtin` source while not being one of these, and the runtime treats the two as separate questions. `disableableNames` is the subset a user may turn off, which a client needs to decide whether to offer a toggle. `yamlBasedNames` is the subset backed by a shipped YAML definition, which a client needs before asking the runtime to load one. + /// + /// Wire method: `agents.getBuiltins`. + /// + /// # Returns + /// + /// The agents this runtime ships, named so a consumer can tell them apart from authored ones. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn get_builtins(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::AGENTS_GETBUILTINS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists the shipped agents a client should offer right now, filtered by the feature flags it passes. `getBuiltins` names every agent the runtime knows about; some of those are gated, so a client rendering a picker wants this narrower list together with the description to show beside each name. + /// + /// Wire method: `agents.getAvailableBuiltins`. + /// + /// # Parameters + /// + /// * `params` - The feature flags to evaluate shipped agents against. + /// + /// # Returns + /// + /// The shipped agents available under the requested flags. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn get_available_builtins( + &self, + params: AgentsGetAvailableBuiltinsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AGENTS_GETAVAILABLEBUILTINS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Loads one shipped agent's YAML definition, for a client that needs what the agent declares rather than only its name. `getBuiltins` reports which names have a definition to load: a name outside its `yamlBasedNames` is special-cased in code and has none. The definition crosses as its own JSON rather than as contract-typed fields, because the runtime parses it with the agent schema's tolerant shape and re-typing it here would drop the keys that shape accepts and this one does not. The projected `__nativeCustomAgent` view the runtime derives is included, so a caller reading the declared model and a caller rendering the agent see the same definition. + /// + /// Wire method: `agents.getBuiltinDefinition`. + /// + /// # Parameters + /// + /// * `params` - The shipped agent whose definition to load. + /// + /// # Returns + /// + /// One shipped agent's definition. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn get_builtin_definition( + &self, + params: AgentsGetBuiltinDefinitionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AGENTS_GETBUILTINDEFINITION, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Projects one shipped agent the way a picker lists it, reading only the metadata at the head of the definition file and stopping before the prompt body. `getBuiltinDefinition` answers the whole definition instead, so a client listing every shipped agent should prefer this one: the cost of a listing grows with the number of agents, and the prompt body is the part a listing never shows. The two also differ in shape. This returns the projected custom agent on its own, whereas `getBuiltinDefinition` returns the authored definition with that projection nested under `__nativeCustomAgent`. + /// + /// Wire method: `agents.getBuiltinListingDefinition`. + /// + /// # Parameters + /// + /// * `params` - The shipped agent whose listing entry to load. + /// + /// # Returns + /// + /// One shipped agent, projected for a listing. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn get_builtin_listing_definition( + &self, + params: AgentsGetBuiltinListingDefinitionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::AGENTS_GETBUILTINLISTINGDEFINITION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Resolves the model a custom agent asks for against the models actually available, and answers both the model to switch to and the warning a user should see when the agent's preference cannot be met. A custom agent may name several acceptable models in preference order, so the decision is a match rather than a lookup, and an agent whose preference is unavailable is a normal outcome that produces a warning rather than an error. A host must call this rather than pick the first available name itself, because the preference order and the wording of the warning are what keep one installation's agent selection the same as another's. + /// + /// Wire method: `agents.customAgentInitialModelDecision`. + /// + /// # Parameters + /// + /// * `params` - The models a custom agent asks for, and the models actually available. + /// + /// # Returns + /// + /// The model to switch to, and the warning to show when the agent's preference could not be met. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn custom_agent_initial_model_decision( + &self, + params: AgentsCustomAgentInitialModelDecisionParams, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call( + rpc_methods::AGENTS_CUSTOMAGENTINITIALMODELDECISION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } } /// `catalog.*` RPCs. @@ -714,24 +857,448 @@ impl<'a> ClientRpcCommands<'a> { } } -/// `environments.*` RPCs. +/// `connectors.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcEnvironments<'a> { +pub struct ClientRpcConnectors<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcEnvironments<'a> { - /// Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. +impl<'a> ClientRpcConnectors<'a> { + /// Returns feature availability. /// - /// Wire method: `environments.list`. + /// Wire method: `connectors.getCapabilities`. + /// + /// # Returns + /// + /// Feature availability. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_capabilities(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::CONNECTORS_GETCAPABILITIES, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns eligible accounts. + /// + /// Wire method: `connectors.getAccounts`. + /// + /// # Returns + /// + /// Eligible accounts. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_accounts(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::CONNECTORS_GETACCOUNTS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists entries for the selected account. + /// + /// Wire method: `connectors.list`. /// /// # Parameters /// - /// * `params` - Optional discovery filters supported by GitHub Mission Control. + /// * `params` - Selected account. /// /// # Returns /// - /// Environments visible to the authenticated caller and matching the supplied filters. + /// Entries for the selected account. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list( + &self, + params: ConnectorDiscoveryAccountRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CONNECTORS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Refreshes entries for the selected account. + /// + /// Wire method: `connectors.refresh`. + /// + /// # Parameters + /// + /// * `params` - Selected account. + /// + /// # Returns + /// + /// Entries for the selected account. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn refresh( + &self, + params: ConnectorDiscoveryAccountRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CONNECTORS_REFRESH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `environments.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcEnvironments<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcEnvironments<'a> { + /// Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. + /// + /// Wire method: `environments.list`. + /// + /// # Parameters + /// + /// * `params` - Optional discovery filters supported by GitHub Mission Control. + /// + /// # Returns + /// + /// Environments visible to the authenticated caller and matching the supplied filters. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list( + &self, + params: EnvironmentsListRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::ENVIRONMENTS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. + /// + /// Wire method: `environments.get`. + /// + /// # Parameters + /// + /// * `params` - Identify a Mission Control environment to retrieve. + /// + /// # Returns + /// + /// Safe discovery information for the requested environment. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get( + &self, + params: EnvironmentsGetRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::ENVIRONMENTS_GET, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. + /// + /// Wire method: `environments.delete`. + /// + /// # Parameters + /// + /// * `params` - Identify a user-managed Mission Control environment to delete. + /// + /// # Returns + /// + /// Acknowledgement that the requested environment was deleted. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn delete( + &self, + params: EnvironmentsDeleteRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::ENVIRONMENTS_DELETE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `extensions.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcExtensions<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcExtensions<'a> { + /// Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. + /// + /// Wire method: `extensions.discover`. + /// + /// # Returns + /// + /// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn discover(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::EXTENSIONS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. + /// + /// Wire method: `extensions.enable`. + /// + /// # Parameters + /// + /// * `params` - Source-qualified extension identifiers to persistently enable for future sessions. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn enable(&self, params: DiscoveredExtensionsEnableRequest) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::EXTENSIONS_ENABLE, Some(wire_params)) + .await?; + Ok(()) + } + + /// Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. + /// + /// Wire method: `extensions.disable`. + /// + /// # Parameters + /// + /// * `params` - Source-qualified extension identifiers to persistently disable for future sessions. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn disable(&self, params: DiscoveredExtensionsDisableRequest) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::EXTENSIONS_DISABLE, Some(wire_params)) + .await?; + Ok(()) + } +} + +/// `git.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcGit<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcGit<'a> { + /// Reads the remote that the branch checked out in a working tree tracks, as `branch..remote` configures it. Reports `origin` rather than failing whenever there is no tracking configuration to read — on a detached HEAD, on a branch with no upstream, or when git itself fails — because a caller asking which remote to talk to needs an answer it can act on, not an error. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// + /// Wire method: `git.currentBranchRemote`. + /// + /// # Parameters + /// + /// * `params` - Working-tree path a git query applies to. + /// + /// # Returns + /// + /// The remote the checked-out branch tracks. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn current_branch_remote( + &self, + params: GitCwdRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::GIT_CURRENTBRANCHREMOTE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Collects the repository context of a working directory in one call: working tree root, repository identifier and host, current branch, and the HEAD and base commits. Every repository field is omitted when the path is not inside a git working tree, and the requested path is echoed back as `cwd`. The answer is the same `SessionWorkingDirectoryContext` that `session.metadata.recordContextChange` accepts, so a caller polling for a context change can forward the result unchanged. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. It can become public once an SDK consumer needs to derive session context from a directory itself. + /// + /// Wire method: `git.workingDirectoryContext`. + /// + /// # Parameters + /// + /// * `params` - Working-tree path a git query applies to. + /// + /// # Returns + /// + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn working_directory_context( + &self, + params: GitCwdRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::GIT_WORKINGDIRECTORYCONTEXT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists the GitHub repositories a working tree's remotes point at, one entry per distinct repository, so a caller can resolve a base and head repository without parsing remote URLs itself. When several remotes name the same repository, only the first is listed, and the entry keeps that remote name. Remotes pointing at no GitHub host are left out, so an empty list means the tree reaches GitHub through no remote. Failing to read the remotes is reported as an error rather than as an empty list, because the two mean different things to a caller. Marked internal because it exists to carry a CLI call site off the napi boundary onto the SDK contract; it is migration plumbing, not a surface consumers are meant to depend on. + /// + /// Wire method: `git.reposFromRemotes`. + /// + /// # Parameters + /// + /// * `params` - Git working tree whose GitHub remotes should be listed. + /// + /// # Returns + /// + /// The GitHub repositories a working tree's remotes point at. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn repos_from_remotes( + &self, + params: GitReposFromRemotesRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::GIT_REPOSFROMREMOTES, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `gitHubOwners.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcGitHubOwners<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcGitHubOwners<'a> { + /// Registers a cancellable owner listing and returns its request id. Separate from `gitHubOwners.list` so the id exists before the listing starts: a caller that abandons the listing the moment it begins would otherwise have nothing to name in `gitHubOwners.cancel`. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + /// + /// Wire method: `gitHubOwners.nextRequestId`. + /// + /// # Returns + /// + /// A freshly registered request id. Registering it before the listing starts is what lets a cancel that races the request still find the owner listing slot. The id serves one listing only. Long-abandoned unused ids can be released by later allocations. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn next_request_id(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::GITHUBOWNERS_NEXTREQUESTID, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists the logins the authenticated user may act as — their own account first, then the organizations they belong to — by asking the GitHub API under the supplied credential. No credential travels in the request: `authInfo` selects one the runtime already holds, and the runtime resolves the token and the GitHub host from it. A failure the caller should render arrives as `message`; one it should raise arrives as `throwError`. + /// + /// Wire method: `gitHubOwners.list`. + /// + /// # Parameters + /// + /// * `params` - Credential to list owners under, and the request id that makes the listing cancellable. + /// + /// # Returns + /// + /// Outcome of an owner listing. Exactly one of `owners` and `message` is present, except that `throwError` reports a failure the caller is expected to raise rather than render. /// ///
/// @@ -740,29 +1307,29 @@ impl<'a> ClientRpcEnvironments<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list( + pub(crate) async fn list( &self, - params: EnvironmentsListRequest, - ) -> Result { + params: GitHubOwnersListRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::ENVIRONMENTS_LIST, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. + /// Abandons an owner listing started with the given request id. Answers `canceled: true` while a listing with that id is running. Answers `canceled: false` when the id was never registered, was registered but not used, was released after being abandoned, or its listing has ended. Canceling an unused id releases it, and a later `list` with that id is refused. The cancel acts only on owner listings and never reaches another request of the host. /// - /// Wire method: `environments.get`. + /// Wire method: `gitHubOwners.cancel`. /// /// # Parameters /// - /// * `params` - Identify a Mission Control environment to retrieve. + /// * `params` - The owner listing to abandon. /// /// # Returns /// - /// Safe discovery information for the requested environment. + /// Whether the id named a running owner listing. /// ///
/// @@ -771,29 +1338,37 @@ impl<'a> ClientRpcEnvironments<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get( + pub(crate) async fn cancel( &self, - params: EnvironmentsGetRequest, - ) -> Result { + params: GitHubOwnersCancelRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::ENVIRONMENTS_GET, Some(wire_params)) + .call(rpc_methods::GITHUBOWNERS_CANCEL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. +/// `gitHubRepository.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcGitHubRepository<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcGitHubRepository<'a> { + /// Resolves the GitHub repository that owns a working-tree path by reading the selected git remote configured for it, preferring `origin`. Returns a null `repository` when the path is inside a git working tree but that selected remote does not resolve to a GitHub host. Fails when the path is not inside a git working tree at all, so a caller can tell 'not a repository' apart from 'a repository with no GitHub remote'. /// - /// Wire method: `environments.delete`. + /// Wire method: `gitHubRepository.atPath`. /// /// # Parameters /// - /// * `params` - Identify a user-managed Mission Control environment to delete. + /// * `params` - Working-tree path whose owning GitHub repository should be resolved. /// /// # Returns /// - /// Acknowledgement that the requested environment was deleted. + /// The GitHub repository that owns the requested path, when the selected remote (`origin`, else the first) is on a GitHub host. /// ///
/// @@ -802,33 +1377,33 @@ impl<'a> ClientRpcEnvironments<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn delete( + pub(crate) async fn at_path( &self, - params: EnvironmentsDeleteRequest, - ) -> Result { + params: GitHubRepositoryAtPathRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::ENVIRONMENTS_DELETE, Some(wire_params)) + .call(rpc_methods::GITHUBREPOSITORY_ATPATH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `extensions.*` RPCs. +/// `globalState.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcExtensions<'a> { +pub struct ClientRpcGlobalState<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcExtensions<'a> { - /// Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. +impl<'a> ClientRpcGlobalState<'a> { + /// Reads the host's machine-wide state: which plugins are installed and the one-off flags and timestamps that record what the user has already been shown or migrated. This is the state that outlives a single session and a single workspace, so a host reads it to decide whether to run a first-launch step, offer an onboarding prompt, or skip one it has already completed. The stored credentials are deliberately not part of this result; a caller that needs an authenticated identity asks the account methods for it instead. Reading is non-destructive and every field is optional, because a fresh install has recorded nothing yet. /// - /// Wire method: `extensions.discover`. + /// Wire method: `globalState.load`. /// /// # Returns /// - /// Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
/// @@ -837,22 +1412,26 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn discover(&self) -> Result { + pub(crate) async fn load(&self) -> Result { let wire_params = serde_json::json!({}); let _value = self .client - .call(rpc_methods::EXTENSIONS_DISCOVER, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_LOAD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. + /// Reads the host's machine-wide state exactly as `globalState.load` does, but from a caller-supplied configuration directory instead of the one the server resolved for itself. Use this when a consumer scopes a session to its own Copilot home — the SDK's per-session `configDir` override — so the state read matches the directory that session actually uses. An absent or empty `configDir` resolves the server's own home, making this identical to `globalState.load`. The stored credentials are omitted here for the same reason they are omitted from `globalState.load`: a caller that needs an authenticated identity asks the account methods instead, so pointing this at another directory cannot be used to read the credentials kept in it. /// - /// Wire method: `extensions.enable`. + /// Wire method: `globalState.loadForConfigDir`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifiers to persistently enable for future sessions. + /// * `params` - Selects the configuration directory whose machine-wide state to read. + /// + /// # Returns + /// + /// The host's machine-wide state. Every field is optional because a fresh install has recorded nothing yet, so a reader must treat an absent field as `not yet`, never as a negative answer. Stored credentials are deliberately absent from this shape. /// ///
/// @@ -861,22 +1440,25 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn enable(&self, params: DiscoveredExtensionsEnableRequest) -> Result<(), Error> { + pub(crate) async fn load_for_config_dir( + &self, + params: GlobalStateLoadForConfigDirRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::EXTENSIONS_ENABLE, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_LOADFORCONFIGDIR, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. + /// Records one top-level key in the host's machine-wide state, the counterpart to `globalState.load`. A host calls this to remember that it has shown an onboarding step, asked a one-off question, or completed a migration, so the next run can skip it. Only the named key is replaced and the rest of the document is preserved, which lets two writers record different flags without overwriting each other; passing no value removes the key instead. Only the keys a host records itself are writable: `appInstallNudgeResponded`, `appTipShown`, `askedSetupTerminals`, `autoFeedbackLastPromptedAt`, `firstLaunchAt`, `recentModelIds`, `sandboxCredentialProxyCaDeclined` and `sandboxOnboardingShown`. Every other key is refused, including `installedPlugins`, the stored credentials, `trustedFolders`, the staff flags and the signed-in accounts. Plugin enablement must use the plugin APIs, which apply repository and managed-policy checks. /// - /// Wire method: `extensions.disable`. + /// Wire method: `globalState.writeKey`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifiers to persistently disable for future sessions. + /// * `params` - A single top-level key to record in the host's machine-wide state. The write replaces only that key and leaves the rest of the document untouched, so two writers recording different one-off flags do not overwrite each other. The stored credential keys cannot be written through this method. /// ///
/// @@ -885,11 +1467,11 @@ impl<'a> ClientRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn disable(&self, params: DiscoveredExtensionsDisableRequest) -> Result<(), Error> { + pub(crate) async fn write_key(&self, params: GlobalStateWriteKeyRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::EXTENSIONS_DISABLE, Some(wire_params)) + .call(rpc_methods::GLOBALSTATE_WRITEKEY, Some(wire_params)) .await?; Ok(()) } @@ -2612,6 +3194,13 @@ pub struct ClientRpcSandbox<'a> { } impl<'a> ClientRpcSandbox<'a> { + /// `sandbox.proxyCa.*` sub-namespace. + pub fn proxy_ca(&self) -> ClientRpcSandboxProxyCa<'a> { + ClientRpcSandboxProxyCa { + client: self.client, + } + } + /// Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command. /// /// Wire method: `sandbox.getHostSupport`. @@ -2637,6 +3226,162 @@ impl<'a> ClientRpcSandbox<'a> { } } +/// `sandbox.proxyCa.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcSandboxProxyCa<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcSandboxProxyCa<'a> { + /// Reports whether the persistent certificate authority of the sandbox credential proxy exists, whether OS trust includes it, and whether it must be rotated. Changes nothing. + /// + /// Wire method: `sandbox.proxyCa.getStatus`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_status( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_GETSTATUS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Creates the persistent certificate authority of the sandbox credential proxy if none is stored, without changing OS trust, and returns the path of its public certificate. Keeps an existing certificate authority, even one that must be rotated. Fails where OS trust is unsupported. Trust it with sandbox.proxyCa.trust: the CLI trusts only the hosts in the saved user settings, so it refuses a certificate authority that also covers hosts from sandboxConfig. + /// + /// Wire method: `sandbox.proxyCa.create`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Result of creating the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn create( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_CREATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Replaces the persistent certificate authority of the sandbox credential proxy with a new one for the current credential hosts. If OS trust included the old one, removes it and trusts the new one, which can show an OS authentication prompt. Running sandboxed tools keep the old certificate authority until they restart. + /// + /// Wire method: `sandbox.proxyCa.rotate`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn rotate( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_ROTATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Adds the persistent certificate authority of the sandbox credential proxy to OS trust, so sandboxed clients that read only OS trust accept the proxy. Call create first. Refuses a certificate authority that is not constrained to the current credential hosts. Can show an OS authentication prompt. + /// + /// Wire method: `sandbox.proxyCa.trust`. + /// + /// # Parameters + /// + /// * `params` - Identifies the credential hosts that the persistent certificate authority of the sandbox credential proxy must cover. The runtime always adds the hosts from the saved user settings. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn trust( + &self, + params: SandboxProxyCaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_TRUST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Removes the persistent certificate authority of the sandbox credential proxy from OS trust. Keeps the stored certificate authority. Can show an OS authentication prompt. Sandboxed clients that read only OS trust then reject the proxy; clients that read the per-process certificate bundle continue to work. + /// + /// Wire method: `sandbox.proxyCa.remove`. + /// + /// # Returns + /// + /// Status of the persistent certificate authority of the sandbox credential proxy. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn remove(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::SANDBOX_PROXYCA_REMOVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `secrets.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcSecrets<'a> { @@ -3324,7 +4069,97 @@ impl<'a> ClientRpcSessions<'a> { /// /// # Returns /// - /// Flush a session's pending events to disk. No-op when no writer exists for the session (e.g., already closed). + /// Flush a session's pending events to disk. No-op when no writer exists for the session (e.g., already closed). + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn save(&self, params: SessionsSaveRequest) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_SAVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Releases the in-use lock held by this process for a session. + /// + /// Wire method: `sessions.releaseLock`. + /// + /// # Parameters + /// + /// * `params` - Session ID whose in-use lock should be released. + /// + /// # Returns + /// + /// Release the in-use lock held by this process for the given session. No-op when this process does not currently hold a lock for the session. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn release_lock( + &self, + params: SessionsReleaseLockRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_RELEASELOCK, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Backfills missing summary and context fields on the supplied session metadata records. + /// + /// Wire method: `sessions.enrichMetadata`. + /// + /// # Parameters + /// + /// * `params` - Session metadata records to enrich with summary and context information. + /// + /// # Returns + /// + /// The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn enrich_metadata( + &self, + params: SessionsEnrichMetadataRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_ENRICHMETADATA, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Creates the workspace record for a session that has not been opened yet. A host that hands a session off to another application — writing the record and then launching that application against the session ID — needs the record on disk before any session exists to carry it, which the session-scoped workspace methods cannot do. Replaces any existing record and resets the checkpoint index. When writing to the local filesystem, a stored `fork_count` survives on disk. Returns the record it built, so a surviving stored `fork_count` can differ from the answer. + /// + /// Wire method: `sessions.createWorkspace`. + /// + /// # Parameters + /// + /// * `params` - Identity, state location and starting context for a workspace record. + /// + /// # Returns + /// + /// The workspace record that was written. /// ///
/// @@ -3333,26 +4168,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn save(&self, params: SessionsSaveRequest) -> Result { + pub(crate) async fn create_workspace( + &self, + params: SessionsCreateWorkspaceRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_SAVE, Some(wire_params)) + .call(rpc_methods::SESSIONS_CREATEWORKSPACE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Releases the in-use lock held by this process for a session. + /// Reads a session's workspace record straight from disk, without opening the session. Resuming by session ID has to know where the session lives before it can connect, so the lookup cannot come from the session-scoped workspace methods, which resolve their location from a live session's context. Returns no record when the file is absent. /// - /// Wire method: `sessions.releaseLock`. + /// Wire method: `sessions.loadWorkspace`. /// /// # Parameters /// - /// * `params` - Session ID whose in-use lock should be released. + /// * `params` - Where the session's state lives, as a root directory and the session ID under it. /// /// # Returns /// - /// Release the in-use lock held by this process for the given session. No-op when this process does not currently hold a lock for the session. + /// The workspace record on disk, omitted when the session has none. /// ///
/// @@ -3361,29 +4199,29 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn release_lock( + pub(crate) async fn load_workspace( &self, - params: SessionsReleaseLockRequest, - ) -> Result { + params: SessionsLoadWorkspaceRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_RELEASELOCK, Some(wire_params)) + .call(rpc_methods::SESSIONS_LOADWORKSPACE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Backfills missing summary and context fields on the supplied session metadata records. + /// Merges fields into a session's workspace record on disk, creating the record when it is absent. The counterpart to `sessions.loadWorkspace`, for the same before-the-session-exists case. It preserves stored workspace-schema fields the request does not supply, does not preserve stored keys outside the workspace schema, and never replaces a stored `fork_count`. /// - /// Wire method: `sessions.enrichMetadata`. + /// Wire method: `sessions.updateWorkspaceFields`. /// /// # Parameters /// - /// * `params` - Session metadata records to enrich with summary and context information. + /// * `params` - Where the session's state lives, plus workspace-schema fields to merge into its workspace record. Stored keys outside the schema are not preserved, and a stored `fork_count` is never replaced. /// /// # Returns /// - /// The enriched metadata records, with summary and context fields backfilled where available. Sessions confirmed empty and unnamed are omitted. + /// The merge completed. The record carries the supplied workspace-schema fields, but a stored `fork_count` stays. /// ///
/// @@ -3392,14 +4230,17 @@ impl<'a> ClientRpcSessions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn enrich_metadata( + pub(crate) async fn update_workspace_fields( &self, - params: SessionsEnrichMetadataRequest, - ) -> Result { + params: SessionsUpdateWorkspaceFieldsRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client - .call(rpc_methods::SESSIONS_ENRICHMETADATA, Some(wire_params)) + .call( + rpc_methods::SESSIONS_UPDATEWORKSPACEFIELDS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } @@ -4243,33 +5084,13 @@ pub struct ClientRpcUserSettings<'a> { } impl<'a> ClientRpcUserSettings<'a> { - /// Drops this runtime process's in-memory user settings cache so the next settings read observes disk. - /// - /// Wire method: `user.settings.reload`. - /// - ///
- /// - /// **Experimental.** This API is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. Pin both the - /// SDK and CLI versions if your code depends on it. - /// - ///
- pub async fn reload(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({}); - let _value = self - .client - .call(rpc_methods::USER_SETTINGS_RELOAD, Some(wire_params)) - .await?; - Ok(()) - } - - /// Lists every known user setting (settings.json overlaid with the legacy config.json, config.json wins), each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. + /// Lists every known user setting from settings.json, each with its effective value, its default, and whether it is at the default — so settings the user has never set still appear with their default value. Does not include repository- or enterprise-managed overrides that the runtime layers on top at session time. /// /// Wire method: `user.settings.get`. /// /// # Returns /// - /// Per-key metadata for every known user setting (settings.json overlaid with the legacy config.json, config.json wins), including settings left at their default. Excludes repository- and enterprise-managed overrides. + /// Per-key metadata for every known user setting in settings.json, including settings left at their default. Excludes repository- and enterprise-managed overrides. /// ///
/// @@ -4287,7 +5108,7 @@ impl<'a> ClientRpcUserSettings<'a> { Ok(serde_json::from_value(_value)?) } - /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. Returns the keys whose new value is shadowed by a legacy config.json entry (config.json wins on read), which the runtime leaves in place — such writes do not take effect until the legacy value is removed. + /// Writes one or more user settings to settings.json, replacing each provided top-level key. A key whose value is null is removed. /// /// Wire method: `user.settings.set`. /// @@ -4295,10 +5116,6 @@ impl<'a> ClientRpcUserSettings<'a> { /// /// * `params` - Partial user settings to write to settings.json. Each top-level key is written individually, replacing the existing value; a key whose value is null is removed. /// - /// # Returns - /// - /// Outcome of writing user settings. - /// ///
/// /// **Experimental.** This API is part of an experimental wire-protocol surface @@ -4306,16 +5123,13 @@ impl<'a> ClientRpcUserSettings<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set( - &self, - params: UserSettingsSetRequest, - ) -> Result { + pub async fn set(&self, params: UserSettingsSetRequest) -> Result<(), Error> { let wire_params = serde_json::to_value(params)?; let _value = self .client .call(rpc_methods::USER_SETTINGS_SET, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } } @@ -4536,6 +5350,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.providers.*` sub-namespace. + pub fn providers(&self) -> SessionRpcProviders<'a> { + SessionRpcProviders { + session: self.session, + } + } + /// `session.queue.*` sub-namespace. pub fn queue(&self) -> SessionRpcQueue<'a> { SessionRpcQueue { @@ -5961,6 +6782,34 @@ impl<'a> SessionRpcConnectors<'a> { Ok(serde_json::from_value(_value)?) } + /// Returns the session account selection, or null. + /// + /// Wire method: `session.connectors.getAccount`. + /// + /// # Returns + /// + /// Session account selection, or null. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_account(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_CONNECTORS_GETACCOUNT, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. /// /// Wire method: `session.connectors.getStatus`. @@ -6223,6 +7072,41 @@ impl<'a> SessionRpcConnectors<'a> { Ok(serde_json::from_value(_value)?) } + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. + /// + /// Wire method: `session.connectors.reconcile`. + /// + /// # Parameters + /// + /// * `params` - Requests authoritative Connector-to-MCP reconciliation for the pinned account. + /// + /// # Returns + /// + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ /// + /// Accepts [`ConnectorReconcileOptions`], including inputs added after [`ConnectorReconcileRequest`]. + pub async fn reconcile_with_options( + &self, + params: ConnectorReconcileOptions, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CONNECTORS_RECONCILE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Reconciles the authoritative Connector catalog into the session MCP projection during startup with a bounded deadline and fail-closed cleanup. /// /// Wire method: `session.connectors.reconcileForStartup`. @@ -6339,13 +7223,13 @@ pub struct SessionRpcCustomizations<'a> { } impl<'a> SessionRpcCustomizations<'a> { - /// Reloads all repository and user customizations for the active session: instructions, plugins and their MCP servers and hooks, custom agents, extensions, and skills. Returns diagnostics from the final skill reload. + /// For local sessions, reconciles repository context and discovered instructions, plugins, skills, agents, hooks, MCP servers, and extensions after files appear or change under the working directory. Independent component failures are returned in outcomes and errors; a rejected call can have partially applied earlier steps. Remote sessions must reload on their agent host instead. The model-facing context is rebuilt on the next turn. /// /// Wire method: `session.customizations.reload`. /// /// # Returns /// - /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + /// Results of reloading discovered session customizations. Inspect outcomes for reloaded, skipped, or failed subsystems; a rejection may follow partial mutation. Changes to the model-facing prompt and tools apply on the next turn. /// ///
/// @@ -6354,7 +7238,7 @@ impl<'a> SessionRpcCustomizations<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload(&self) -> Result { + pub async fn reload(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session @@ -7452,10 +8336,14 @@ impl<'a> SessionRpcInstructions<'a> { Ok(serde_json::from_value(_value)?) } - /// Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk. + /// For local sessions, invalidates instruction discovery and the model-facing prompt, then returns freshly discovered sources. The updated prompt takes effect on the next turn. Remote sessions must reload on their agent host instead. /// /// Wire method: `session.instructions.reload`. /// + /// # Returns + /// + /// Instruction sources loaded for the session, in merge order. + /// ///
/// /// **Experimental.** This API is part of an experimental wire-protocol surface @@ -7463,14 +8351,14 @@ impl<'a> SessionRpcInstructions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload(&self) -> Result<(), Error> { + pub async fn reload(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call(rpc_methods::SESSION_INSTRUCTIONS_RELOAD, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } } @@ -7641,6 +8529,13 @@ impl<'a> SessionRpcMcp<'a> { } } + /// `session.mcp.prompts.*` sub-namespace. + pub fn prompts(&self) -> SessionRpcMcpPrompts<'a> { + SessionRpcMcpPrompts { + session: self.session, + } + } + /// `session.mcp.resources.*` sub-namespace. pub fn resources(&self) -> SessionRpcMcpResources<'a> { SessionRpcMcpResources { @@ -7648,7 +8543,39 @@ impl<'a> SessionRpcMcp<'a> { } } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Records the IDE the host is connected to, so the agent's system prompt can name it and its workspace folder. Null or an omitted `ide` clears the recorded value, which is how a host reports that it is disconnected; there is no separate clear method. Both `ideName` and `workspaceFolder` are required together, because half a state cannot be attributed to a project. + /// + /// Wire method: `session.mcp.setConnectedIdeInfo`. + /// + /// # Parameters + /// + /// * `params` - Records which IDE the host is connected to, or clears it. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn set_connected_ide_info( + &self, + params: SessionMcpSetConnectedIdeInfoParams, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_SETCONNECTEDIDEINFO, + Some(wire_params), + ) + .await?; + Ok(()) + } + + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// /// Wire method: `session.mcp.list`. /// @@ -7673,6 +8600,31 @@ impl<'a> SessionRpcMcp<'a> { Ok(serde_json::from_value(_value)?) } + /// Lists effective MCP configuration without starting, restarting, authenticating, or waiting for servers. An optional live observation is from an already materialized matching server; this is not a readiness guarantee. + /// + /// Wire method: `session.mcp.listConfigured`. + /// + /// # Returns + /// + /// Effective MCP configuration with optional live observations from matching already materialized servers. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list_configured(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_LISTCONFIGURED, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// /// Wire method: `session.mcp.listTools`. @@ -8691,7 +9643,7 @@ impl<'a> SessionRpcMcpOauth<'a> { /// /// # Parameters /// - /// * `params` - Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + /// * `params` - Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. /// /// # Returns /// @@ -8721,7 +9673,7 @@ impl<'a> SessionRpcMcpOauth<'a> { /// /// # Parameters /// - /// * `params` - Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + /// * `params` - Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection. /// /// # Returns /// @@ -8750,6 +9702,32 @@ impl<'a> SessionRpcMcpOauth<'a> { Ok(serde_json::from_value(_value)?) } + /// Completes a runtime-managed MCP OAuth login after the authorization server redirects to a host-managed callback URL. + /// + /// Wire method: `session.mcp.oauth.complete`. + /// + /// # Parameters + /// + /// * `params` - Host-delivered callback for a runtime-managed MCP OAuth login. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn complete(&self, params: McpOauthCompleteRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_OAUTH_COMPLETE, Some(wire_params)) + .await?; + Ok(()) + } + /// Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. /// /// Wire method: `session.mcp.oauth.probe`. @@ -8825,7 +9803,84 @@ impl<'a> SessionRpcMcpOauth<'a> { /// /// # Returns /// - /// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. + /// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn cancel_login( + &self, + params: SessionMcpOauthCancelLoginParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_OAUTH_CANCELLOGIN, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Responds to a pending MCP OAuth authorization request by its request id. + /// + /// Wire method: `session.mcp.oauth.respond`. + /// + /// # Parameters + /// + /// * `params` - Pending MCP OAuth request id to respond to. + /// + /// # Returns + /// + /// Indicates whether the pending MCP OAuth response was accepted. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn respond( + &self, + params: McpOauthRespondRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_OAUTH_RESPOND, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.mcp.prompts.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpPrompts<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpPrompts<'a> { + /// Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// + /// Wire method: `session.mcp.prompts.list`. + /// + /// # Parameters + /// + /// * `params` - MCP server whose prompts to enumerate. + /// + /// # Returns + /// + /// One page of prompts advertised by the named MCP server. /// ///
/// @@ -8834,34 +9889,28 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn cancel_login( - &self, - params: SessionMcpOauthCancelLoginParams, - ) -> Result { + pub async fn list(&self, params: McpPromptsListRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_OAUTH_CANCELLOGIN, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_PROMPTS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Responds to a pending MCP OAuth authorization request by its request id. + /// Get a prompt's messages from a connected MCP server (proxies MCP `prompts/get`). Content is preserved as opaque JSON. Does not send messages to the model, execute tools, or fetch referenced resources. /// - /// Wire method: `session.mcp.oauth.respond`. + /// Wire method: `session.mcp.prompts.get`. /// /// # Parameters /// - /// * `params` - Pending MCP OAuth request id to respond to. + /// * `params` - MCP server, prompt name, and optional string-valued arguments. /// /// # Returns /// - /// Indicates whether the pending MCP OAuth response was accepted. + /// Prompt messages returned by the MCP server without sending them to the model. /// ///
/// @@ -8870,16 +9919,13 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn respond( - &self, - params: McpOauthRespondRequest, - ) -> Result { + pub async fn get(&self, params: McpPromptsGetRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_OAUTH_RESPOND, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_PROMPTS_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } @@ -11332,6 +12378,192 @@ impl<'a> SessionRpcProvider<'a> { } } +/// `session.providers.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcProviders<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcProviders<'a> { + /// `session.providers.models.*` sub-namespace. + pub fn models(&self) -> SessionRpcProvidersModels<'a> { + SessionRpcProvidersModels { + session: self.session, + } + } + + /// Returns adapter definitions and supported operations in this session's effective provider catalog, without running discovery. Does not list provider instances or select inference models. + /// + /// Wire method: `session.providers.getCatalog`. + /// + /// # Returns + /// + /// Normalized model-provider adapter definitions available to the session, not discovered instances. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_catalog(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PROVIDERS_GETCATALOG, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + /// + /// Wire method: `session.providers.discover`. + /// + /// # Parameters + /// + /// * `params` - Provider discovery parameters. + /// + /// # Returns + /// + /// Provider instances found by a discovery operation. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn discover( + &self, + params: ModelProviderDiscoverRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PROVIDERS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Gets current health and version information for a discovered model-provider instance. + /// + /// Wire method: `session.providers.getStatus`. + /// + /// # Parameters + /// + /// * `params` - Provider status request parameters. + /// + /// # Returns + /// + /// Current health information for a provider instance. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_status( + &self, + params: ModelProviderGetStatusRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PROVIDERS_GETSTATUS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.providers.models.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcProvidersModels<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcProvidersModels<'a> { + /// Lists models installed or otherwise available from a discovered model-provider instance. + /// + /// Wire method: `session.providers.models.list`. + /// + /// # Parameters + /// + /// * `params` - Provider model inventory request parameters. + /// + /// # Returns + /// + /// Models offered for agent conversations by one provider instance. Adapters exclude known-incompatible models, but retain candidates with unknown capabilities. Listing does not guarantee compatibility. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list( + &self, + params: ModelProviderModelsListRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_LIST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Translates a discovered model into the provider and model configuration needed to use it, and reports whether each is already registered in this session. Prepares only: it registers nothing, writes nothing, and performs no provider requests. + /// + /// Wire method: `session.providers.models.prepareConfiguration`. + /// + /// # Parameters + /// + /// * `params` - A discovered instance and one of its models to translate into provider configuration. Pass back the instance and model as returned by `session.providers.discover` and `session.providers.models.list`. + /// + /// # Returns + /// + /// Provider configuration prepared from a discovered model. Preparing a plan changes nothing: it neither registers the model with the session nor writes durable configuration. To apply it, pass `provider` and `model` to `session.provider.add`, omitting whichever the dispositions report as already configured. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn prepare_configuration( + &self, + params: ModelProviderPrepareConfigurationRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_PREPARECONFIGURATION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.queue.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcQueue<'a> { @@ -12504,17 +13736,17 @@ pub struct SessionRpcShell<'a> { } impl<'a> SessionRpcShell<'a> { - /// Starts a shell command and streams output through session notifications. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. + /// Starts a shell command, returning an RPC error if it cannot be spawned. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. /// /// Wire method: `session.shell.exec`. /// /// # Parameters /// - /// * `params` - Shell command to run, with optional working directory and timeout in milliseconds. + /// * `params` - Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. /// /// # Returns /// - /// Identifier of the spawned process, used to correlate streamed output and exit notifications. + /// Identifier of the spawned shell process, usable with shell.kill while the process is running. /// ///
/// @@ -13637,6 +14869,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending elicitation request after direct interaction in the trusted in-process client. Only an accepted response to the built-in ask_user tool can become trusted human evidence. + /// + /// Wire method: `session.ui.handleHumanAskUser`. + /// + /// # Parameters + /// + /// * `params` - Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + /// + /// # Returns + /// + /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn handle_human_ask_user( + &self, + params: UIHandlePendingElicitationRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANASKUSER, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves a pending `user_input.requested` event with the user's response. /// /// Wire method: `session.ui.handlePendingUserInput`. @@ -13673,6 +14941,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + /// + /// Wire method: `session.ui.handleHumanUserInput`. + /// + /// # Parameters + /// + /// * `params` - Request ID of a pending `user_input.requested` event and the user's response. + /// + /// # Returns + /// + /// Indicates whether the pending UI request was resolved by this call. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn handle_human_user_input( + &self, + params: UIHandlePendingUserInputRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANUSERINPUT, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. /// /// Wire method: `session.ui.handlePendingSampling`. @@ -13817,6 +15121,42 @@ impl<'a> SessionRpcUi<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + /// + /// Wire method: `session.ui.handleHumanExitPlanMode`. + /// + /// # Parameters + /// + /// * `params` - Request ID of a pending `exit_plan_mode.requested` event and the user's response. + /// + /// # Returns + /// + /// Indicates whether the pending UI request was resolved by this call. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub(crate) async fn handle_human_exit_plan_mode( + &self, + params: UIHandlePendingExitPlanModeRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_UI_HANDLEHUMANEXITPLANMODE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. /// /// Wire method: `session.ui.registerDirectAutoModeSwitchHandler`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 613d99329e..b9f6e0ca73 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -228,8 +228,12 @@ pub enum SessionEventType { ToolUserRequested, #[serde(rename = "tool.execution_start")] ToolExecutionStart, + #[doc(hidden)] + #[deprecated] #[serde(rename = "tool.execution_partial_result")] ToolExecutionPartialResult, + #[serde(rename = "tool.shell_output")] + ToolShellOutput, #[serde(rename = "tool.execution_progress")] ToolExecutionProgress, #[serde(rename = "tool.execution_complete")] @@ -440,6 +444,8 @@ pub enum SessionEventType { ExitPlanModeRequested, #[serde(rename = "exit_plan_mode.completed")] ExitPlanModeCompleted, + #[serde(rename = "human_response.recorded")] + HumanResponseRecorded, #[serde(rename = "session.tools_updated")] SessionToolsUpdated, #[serde(rename = "session.background_tasks_changed")] @@ -778,8 +784,12 @@ pub enum SessionEventData { ToolUserRequested(ToolUserRequestedData), #[serde(rename = "tool.execution_start")] ToolExecutionStart(ToolExecutionStartData), + #[doc(hidden)] + #[deprecated] #[serde(rename = "tool.execution_partial_result")] ToolExecutionPartialResult(ToolExecutionPartialResultData), + #[serde(rename = "tool.shell_output")] + ToolShellOutput(ToolShellOutputData), #[serde(rename = "tool.execution_progress")] ToolExecutionProgress(ToolExecutionProgressData), #[serde(rename = "tool.execution_complete")] @@ -962,6 +972,8 @@ pub enum SessionEventData { ExitPlanModeRequested(ExitPlanModeRequestedData), #[serde(rename = "exit_plan_mode.completed")] ExitPlanModeCompleted(ExitPlanModeCompletedData), + #[serde(rename = "human_response.recorded")] + HumanResponseRecorded(HumanResponseRecordedData), #[serde(rename = "session.tools_updated")] SessionToolsUpdated(SessionToolsUpdatedData), #[serde(rename = "session.background_tasks_changed")] @@ -1188,6 +1200,9 @@ pub struct SessionStartData { /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_model: Option, /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, @@ -1238,6 +1253,9 @@ pub struct SessionResumeData { /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_model: Option, /// Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, @@ -1534,6 +1552,9 @@ pub struct SessionModelChangeData { /// Reasoning effort level after the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, + /// Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort_model: Option, /// Reasoning summary mode after the model change, if applicable #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, @@ -3230,6 +3251,9 @@ pub struct AssistantMessageReasoningBlocks { /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. #[serde(skip_serializing_if = "Option::is_none")] pub blocks: Option>, + /// Anthropic Messages assistant block ordering preserved when the legacy reasoning-only representation cannot reproduce it exactly. Thinking and text blocks remain verbatim; tool-use entries retain identity and a payload fingerprint when later signed reasoning depends on them, and are hydrated from the message's tool requests during replay. + #[serde(skip_serializing_if = "Option::is_none")] + pub ordered_blocks: Option>, /// Model provider that produced these reasoning blocks. pub provider: String, } @@ -3538,6 +3562,9 @@ pub struct AssistantUsageData { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) available_tool_count: Option, + /// Where the bring-your-own-key model runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + #[serde(skip_serializing_if = "Option::is_none")] + pub byok_kind: Option, /// Whether the provider reported prompt-cache usage details for this call #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] @@ -3617,6 +3644,9 @@ pub struct AssistantUsageData { pub max_prompt_tokens: Option, /// Model identifier used for this API call pub model: String, + /// Fixed-set provider family serving the bring-your-own-key model (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// Number of tool calls returned by the model #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] @@ -3749,6 +3779,10 @@ pub struct PromptCacheBreakData { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) tools_redefined: Option>, + /// Changed definition parts of redefined tools, as `tool:part` entries; property-level parts only for telemetry-safe tools, whose other names are hashed + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) tools_redefined_parts: Option>, /// Raw names of tools redefined since the prior call, restricted because a tool name can be user-authored #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] @@ -3801,6 +3835,9 @@ pub struct ModelCallFailureData { /// For HTTP 400 failures only: whether the response carried a structured CAPI error envelope (structured_error, a deterministic validation failure) or no error body (bodyless, the transient gateway/proxy signature). Absent for non-400 failures. #[serde(skip_serializing_if = "Option::is_none")] pub bad_request_kind: Option, + /// Where the bring-your-own-key model for the failed call runs and who manages it: "local_managed" (on the device, managed by Copilot), "local_user" (on the device, managed by the user), or "remote_user" (off the device, managed by the user). Absent for Copilot-served models. + #[serde(skip_serializing_if = "Option::is_none")] + pub byok_kind: Option, /// Duration of the failed API call in milliseconds #[serde(skip_serializing_if = "Option::is_none")] pub duration_ms: Option, @@ -3847,6 +3884,9 @@ pub struct ModelCallFailureData { /// Model identifier used for the failed API call #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Fixed-set provider family serving the bring-your-own-key model for the failed call (for example "openai", "anthropic", "azure_openai", "ollama", "llama_cpp", or "other"). Never the caller-supplied provider name. Absent for Copilot-served models. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// Parent task tool call ID when this failed model call belongs to a sub-agent #[serde(skip_serializing_if = "Option::is_none")] pub parent_tool_call_id: Option, @@ -4080,22 +4120,49 @@ pub struct ToolExecutionStartData { pub turn_id: Option, } -/// Session event "tool.execution_partial_result". Streaming tool execution output for incremental result display +/// Session event "tool.execution_partial_result". Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. +#[doc(hidden)] +#[deprecated] #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ToolExecutionPartialResultData { - /// Incremental output chunk from the running tool + /// Merged replacement snapshot from the running shell, not an append-only chunk pub partial_output: String, /// Tool call ID this partial result belongs to pub tool_call_id: String, } +/// Session event "tool.shell_output". Live, append-only shell output. Not persisted or replayed to late subscribers. Text is decoded and redacted per chunk; chunks need not contain complete lines. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolShellOutputData { + /// Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + pub sequence: i64, + /// Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. + #[serde(skip_serializing_if = "Option::is_none")] + pub stream: Option, + /// New output to append, without synthetic shell-result markers or stream-switch separators + pub text: String, + /// Tool call ID that owns this shell output + pub tool_call_id: String, +} + /// Session event "tool.execution_progress". Tool execution progress notification with status message #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ToolExecutionProgressData { /// Human-readable progress status message (e.g., from an MCP server) pub progress_message: String, + /// Client-only structured progress metadata. Not model-facing tool output. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub structured_content: Option, /// Tool call ID this progress notification belongs to pub tool_call_id: String, } @@ -4114,6 +4181,23 @@ pub struct ToolExecutionCompleteError { pub remediation: Option, } +/// A file mutation that was actually committed by a built-in file editing tool. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteFileEdit { + /// Kind of mutation committed at this path. + pub kind: ToolExecutionCompleteFileEditKind, + /// Absolute path in the session filesystem namespace. + pub path: String, +} + /// Binary result returned by a tool for the model #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4577,6 +4661,16 @@ pub struct ToolExecutionCompleteData { /// Error details when the tool execution failed #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, + /// Experimental. File mutations actually committed by a built-in file editing tool, in execution order. Present on successful edits and on partial failures when earlier mutations were committed. Paths are absolute in the session filesystem namespace. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub file_edits: Option>, /// Experimental HydraFusion attribution for this tool completion. /// ///
@@ -7410,6 +7504,9 @@ pub struct SessionAutoModeResolvedData { /// The routing method the server applied, when Auto Intent ran #[serde(skip_serializing_if = "Option::is_none")] pub routing_method: Option, + /// Short human-readable sentence from the routing service explaining why this model was chosen, for display alongside the model. Present only when the service supplied one: it is omitted for on-device selections, when the service did not provide an explanation, and when a replayed decision made no routing call. The text is display-only and drawn from a fixed catalogue; several distinct routing categories share identical wording, so it cannot be used to recover the category or keyed on programmatically. + #[serde(skip_serializing_if = "Option::is_none")] + pub selection_reason: Option, /// Whether a sticky model choice overrode the router result #[serde(skip_serializing_if = "Option::is_none")] pub sticky_override: Option, @@ -7561,6 +7658,79 @@ pub struct ExitPlanModeCompletedData { pub selected_action: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedResponseAskUser { + /// Exact answer content accepted from the user. + pub content: HashMap, + /// Exact question displayed to the user. + pub message: String, + /// Exact response schema displayed to the user. + pub requested_schema: ElicitationRequestedSchema, + /// Runtime-owned response kind discriminator. + pub response_kind: HumanResponseRecordedResponseAskUserResponseKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedResponseUserInput { + /// Whether the displayed request allowed a free-form answer. + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_freeform: Option, + /// Exact selected or free-form answer submitted by the user. + pub answer: String, + /// Exact choices displayed to the user, when the request offered choices. + #[serde(skip_serializing_if = "Option::is_none")] + pub choices: Option>, + /// Exact question displayed to the user. + pub question: String, + /// Runtime-owned response kind discriminator. + pub response_kind: HumanResponseRecordedResponseUserInputResponseKind, + /// Whether the answer was typed as free-form text rather than selected from the displayed choices. + pub was_freeform: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedResponseExitPlanMode { + /// Actions offered by the plan review UI. + pub actions: Vec, + /// Whether the user approved the reviewed plan. + pub approved: bool, + /// Whether the selected response requested edit auto-approval. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_approve_edits: Option, + /// Exact feedback submitted with the plan decision, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub feedback: Option, + /// Exact full plan content available from the review UI. + pub plan_content: String, + /// Action the plan review UI recommended. + pub recommended_action: ExitPlanModeAction, + /// Runtime-owned response kind discriminator. + pub response_kind: HumanResponseRecordedResponseExitPlanModeResponseKind, + /// Action selected by the user, when applicable. + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_action: Option, + /// Exact plan summary displayed to the user. + pub summary: String, +} + +/// Session event "human_response.recorded". Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HumanResponseRecordedData { + /// Controlled actor provenance established at response ingress. + pub actor: HumanResponseActor, + /// Request ID of the runtime-owned question or plan review. + pub request_id: RequestId, + /// Typed request and response payload. + pub response: HumanResponseRecordedResponse, + /// Tool call ID that opened the request, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + /// Session event "session.tools_updated". Payload of `session.tools_updated` identifying the model whose resolved tools were updated. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -7764,7 +7934,7 @@ pub struct SessionMcpServersLoadedData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMcpServerStatusChangedData { - /// Runtime configuration provenance for a failed connection, or unknown when unavailable. Additional string values may be introduced. + /// Runtime configuration provenance for a connected or failed server, or unknown when unavailable. Additional string values may be introduced. #[serde(skip_serializing_if = "Option::is_none")] pub config_source: Option, /// Error message if the server entered a failed state @@ -9367,7 +9537,7 @@ pub enum AbortReason { Unknown, } -/// Configuration source: user, workspace, plugin, builtin, or managed +/// Configuration source: user, workspace, plugin, builtin, managed, or account #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpServerSource { /// Server configured in the user's global MCP configuration. @@ -9385,6 +9555,9 @@ pub enum McpServerSource { /// Server supplied by a trusted host-managed catalog. #[serde(rename = "managed")] Managed, + /// Server contributed by a signed-in account; enablement and organization policy still apply. + #[serde(rename = "account")] + Account, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -9427,6 +9600,70 @@ pub enum ToolExecutionStartToolDescriptionMetaUIVisibility { Unknown, } +/// Shell output source. Terminal output has no separate stdout/stderr attribution. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ToolShellOutputStream { + /// Output from the shell command's standard output stream. This is the default when stream is omitted. + #[serde(rename = "stdout")] + Stdout, + /// Output from the shell command's standard error stream. + #[serde(rename = "stderr")] + Stderr, + /// Inherently merged output that cannot be attributed separately to stdout or stderr. + #[serde(rename = "terminal")] + Terminal, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Kind of file mutation committed by a built-in editing tool. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub enum ToolExecutionCompleteFileEditKind { + /// A file was created. + Create, + /// A file was written by an edit operation. + Edit, + /// A file was deleted. + Delete, + /// An unrecognized operation kind, retaining its wire value. + Unknown(String), +} +impl Default for ToolExecutionCompleteFileEditKind { + fn default() -> Self { + Self::Unknown("unknown".to_owned()) + } +} +impl From for ToolExecutionCompleteFileEditKind { + fn from(value: String) -> Self { + match value.as_str() { + "create" => Self::Create, + "edit" => Self::Edit, + "delete" => Self::Delete, + _ => Self::Unknown(value), + } + } +} +impl From for String { + fn from(value: ToolExecutionCompleteFileEditKind) -> Self { + match value { + ToolExecutionCompleteFileEditKind::Create => "create".to_owned(), + ToolExecutionCompleteFileEditKind::Edit => "edit".to_owned(), + ToolExecutionCompleteFileEditKind::Delete => "delete".to_owned(), + ToolExecutionCompleteFileEditKind::Unknown(value) => value, + } + } +} + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PersistedBinaryImageType { @@ -10260,6 +10497,9 @@ pub enum PermissionApprovalEvaluationReasonCode { /// The script path was not authorized for inspection. #[serde(rename = "path-not-authorized")] PathNotAuthorized, + /// A code source was excluded from review by content exclusion policy. + #[serde(rename = "content-excluded")] + ContentExcluded, /// The script working directory was invalid. #[serde(rename = "invalid-working-directory")] InvalidWorkingDirectory, @@ -10296,6 +10536,24 @@ pub enum PermissionApprovalEvaluationReasonCode { /// The script argument binding could not be reviewed. #[serde(rename = "argument-binding-unreviewable")] ArgumentBindingUnreviewable, + /// The shell command could not be analyzed for execution evidence. + #[serde(rename = "unsupported-command-shape")] + UnsupportedCommandShape, + /// The shell command used a code source that cannot be bound for review. + #[serde(rename = "unsupported-source")] + UnsupportedSource, + /// The shell command used a code source computed at run time. + #[serde(rename = "dynamic-source")] + DynamicSource, + /// The shell command referenced more code sources than can be reviewed. + #[serde(rename = "too-many-sources")] + TooManySources, + /// A code-bearing executable could not be inspected. + #[serde(rename = "executable-unavailable")] + ExecutableUnavailable, + /// A code-bearing executable exceeded the binding size limit. + #[serde(rename = "executable-too-large")] + ExecutableTooLarge, /// The script review metadata was malformed. #[serde(rename = "malformed-script-action-review")] MalformedScriptActionReview, @@ -11178,6 +11436,57 @@ pub enum ExitPlanModeAction { Unknown, } +/// Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseActor { + /// A built-in trusted client submitted the response after direct human interaction. + #[serde(rename = "human_response")] + HumanResponse, + /// A host or SDK automation submitted the response without direct human interaction. + #[serde(rename = "host_automation")] + HostAutomation, + /// The response came through a legacy or otherwise unattributed ingress. + #[serde(rename = "unknown")] + UnknownValue, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Runtime-owned response kind discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseRecordedResponseAskUserResponseKind { + #[serde(rename = "ask_user")] + #[default] + AskUser, +} + +/// Runtime-owned response kind discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseRecordedResponseUserInputResponseKind { + #[serde(rename = "user_input")] + #[default] + UserInput, +} + +/// Runtime-owned response kind discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HumanResponseRecordedResponseExitPlanModeResponseKind { + #[serde(rename = "exit_plan_mode")] + #[default] + ExitPlanMode, +} + +/// Exact runtime-owned question or reviewed plan paired with the typed response that settled it. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum HumanResponseRecordedResponse { + AskUser(HumanResponseRecordedResponseAskUser), + UserInput(HumanResponseRecordedResponseUserInput), + ExitPlanMode(HumanResponseRecordedResponseExitPlanMode), +} + /// Terminal status a workflow run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WorkflowRunSettledStatus { @@ -11268,7 +11577,7 @@ pub enum McpServerStatus { /// The server is configured but disabled. #[serde(rename = "disabled")] Disabled, - /// The server was intentionally stopped and can be restarted on demand when policy permits; a server quarantined by restrictive managed policy stays stopped and cannot be restarted until the policy allows it. + /// The server is not running: it may not have started yet, may have been explicitly stopped, or may be quarantined by restrictive managed policy. It can be restarted on demand when policy permits. #[serde(rename = "stopped")] Stopped, /// The server is not configured for this session. diff --git a/rust/src/github_token.rs b/rust/src/github_token.rs index e5cd201b83..db77cce96c 100644 --- a/rust/src/github_token.rs +++ b/rust/src/github_token.rs @@ -418,33 +418,4 @@ async fn send_error(client: &Weak, request_id: u64, code: i32, mess } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn token_debug_is_redacted() { - let token = GitHubToken::new("do-not-print", 28_800); - assert!(!format!("{token:?}").contains("do-not-print")); - } - - #[test] - fn retiring_session_removes_its_provider() { - let registry = GitHubTokenRegistry::new(); - let provider = Arc::new(|_args: GitHubTokenProviderArgs| async { - Ok(GitHubTokenProviderResult::Cancelled) - }); - let registration_id = registry.register(provider); - let session_id = crate::SessionId::from("session-1"); - registry.claim(®istration_id, session_id.clone()); - - registry.retire_session(&session_id); - - assert!( - !registry - .state - .lock() - .providers - .contains_key(®istration_id) - ); - } -} +mod tests; diff --git a/rust/src/github_token/tests.rs b/rust/src/github_token/tests.rs new file mode 100644 index 0000000000..d1376f5360 --- /dev/null +++ b/rust/src/github_token/tests.rs @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn token_debug_is_redacted() { + let token = GitHubToken::new("do-not-print", 28_800); + assert!(!format!("{token:?}").contains("do-not-print")); +} + +#[test] +fn retiring_session_removes_its_provider() { + let registry = GitHubTokenRegistry::new(); + let provider = Arc::new(|_args: GitHubTokenProviderArgs| async { + Ok(GitHubTokenProviderResult::Cancelled) + }); + let registration_id = registry.register(provider); + let session_id = crate::SessionId::from("session-1"); + registry.claim(®istration_id, session_id.clone()); + + registry.retire_session(&session_id); + + assert!( + !registry + .state + .lock() + .providers + .contains_key(®istration_id) + ); +} diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 61d9b192cb..a21dee9f87 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -381,97 +381,4 @@ impl PermissionHandler for DenyAllHandler { } #[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn approve_all_handler_returns_approved() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData::default(), - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_handler_fails_when_managed_settings_enabled() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData { - managed_settings_enabled: true, - ..Default::default() - }, - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::UserNotAvailable(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_handler_leaves_managed_approval_pending() { - let result = ApproveAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData { - managed_approval_required: Some(true), - ..Default::default() - }, - ) - .await; - assert!(matches!(result, PermissionResult::NoResult)); - } - - #[tokio::test] - async fn deny_all_handler_returns_denied() { - let result = DenyAllHandler - .handle( - SessionId::from("s1"), - RequestId::new("1"), - PermissionRequestData::default(), - ) - .await; - assert!(matches!( - result, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[test] - fn mcp_auth_result_token_converts_to_wire_response() { - let wire = McpAuthResult::Token { - access_token: "host-token".to_string(), - token_type: Some("Bearer".to_string()), - expires_in: Some(3600), - } - .into_wire(); - - match wire { - McpOauthPendingRequestResponse::Token(token) => { - assert_eq!(token.access_token, "host-token"); - assert_eq!(token.token_type.as_deref(), Some("Bearer")); - assert_eq!(token.expires_in, Some(3600)); - } - McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"), - } - } -} +mod tests; diff --git a/rust/src/handler/tests.rs b/rust/src/handler/tests.rs new file mode 100644 index 0000000000..1d1df233bc --- /dev/null +++ b/rust/src/handler/tests.rs @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[tokio::test] +async fn approve_all_handler_returns_approved() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_handler_fails_when_managed_settings_enabled() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData { + managed_settings_enabled: true, + ..Default::default() + }, + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::UserNotAvailable(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_handler_leaves_managed_approval_pending() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData { + managed_approval_required: Some(true), + ..Default::default() + }, + ) + .await; + assert!(matches!(result, PermissionResult::NoResult)); +} + +#[tokio::test] +async fn deny_all_handler_returns_denied() { + let result = DenyAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) + .await; + assert!(matches!( + result, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[test] +fn mcp_auth_result_token_converts_to_wire_response() { + let wire = McpAuthResult::Token { + access_token: "host-token".to_string(), + token_type: Some("Bearer".to_string()), + expires_in: Some(3600), + } + .into_wire(); + + match wire { + McpOauthPendingRequestResponse::Token(token) => { + assert_eq!(token.access_token, "host-token"); + assert_eq!(token.token_type.as_deref(), Some("Bearer")); + assert_eq!(token.expires_in, Some(3600)); + } + McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"), + } +} diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs index 4986d6cb18..3b2af37cb1 100644 --- a/rust/src/hooks.rs +++ b/rust/src/hooks.rs @@ -362,6 +362,86 @@ pub struct AgentStopOutput { pub reason: Option, } +/// Input for `subagentStart`, received before a subagent's first turn. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStartInput { + /// The parent session ID. + pub session_id: String, + /// Unix timestamp in ms. + pub timestamp: f64, + /// Working directory of the parent session. + #[serde(rename = "cwd")] + pub working_directory: PathBuf, + /// Path to the parent session transcript. + pub transcript_path: PathBuf, + /// Name of the subagent definition. + pub agent_name: String, + /// Display name, when the definition provides one. + #[serde(default)] + pub agent_display_name: Option, + /// Description, when the definition provides one. + #[serde(default)] + pub agent_description: Option, +} + +/// Output for `subagentStart`. +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStartOutput { + /// Context prepended to the subagent's first prompt. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_context: Option, +} + +/// Input for `subagentStop`, received after a subagent's turn. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStopInput { + /// The parent session ID. + pub session_id: String, + /// Unix timestamp in ms. + pub timestamp: f64, + /// Working directory of the parent session. + #[serde(rename = "cwd")] + pub working_directory: PathBuf, + /// Path to the parent session transcript. + pub transcript_path: PathBuf, + /// Name of the subagent definition. + pub agent_name: String, + /// Type of the subagent. + pub agent_type: String, + /// Agent ID, when available. + #[serde(default)] + pub agent_id: Option, + /// Display name, when the definition provides one. + #[serde(default)] + pub agent_display_name: Option, + /// Description, when the definition provides one. + #[serde(default)] + pub agent_description: Option, + /// Reason the subagent stopped (normally `"end_turn"`). + pub stop_reason: String, + /// The subagent's last assistant response. + pub response: String, +} + +/// Output for `subagentStop`. +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentStopOutput { + /// Set to `"block"` to run another subagent turn; only `"allow"` and `"block"` are valid. + #[serde(skip_serializing_if = "Option::is_none")] + pub decision: Option, + /// Nonempty follow-up instruction required when blocking; without a block + /// decision this is an error, not an instruction to the child. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Replacement final response when the stop is allowed. + #[serde(skip_serializing_if = "Option::is_none")] + pub modified_response: Option, +} + /// Events dispatched to [`SessionHooks::on_hook`] at CLI lifecycle points. /// /// Each variant carries the typed input for that hook plus the shared @@ -442,6 +522,20 @@ pub enum HookEvent { /// Session context. ctx: HookContext, }, + /// Fired before a subagent's first turn. + SubagentStart { + /// Typed input data. + input: SubagentStartInput, + /// Session context. + ctx: HookContext, + }, + /// Fired after a subagent's turn. + SubagentStop { + /// Typed input data. + input: SubagentStopInput, + /// Session context. + ctx: HookContext, + }, } /// Response from [`SessionHooks::on_hook`] back to the SDK. @@ -473,6 +567,10 @@ pub enum HookOutput { ErrorOccurred(ErrorOccurredOutput), /// Response for an agent-stop hook. AgentStop(AgentStopOutput), + /// Response for a subagent-start hook. + SubagentStart(SubagentStartOutput), + /// Response for a subagent-stop hook. + SubagentStop(SubagentStopOutput), } impl HookOutput { @@ -489,6 +587,8 @@ impl HookOutput { Self::SessionEnd(_) => "SessionEnd", Self::ErrorOccurred(_) => "ErrorOccurred", Self::AgentStop(_) => "AgentStop", + Self::SubagentStart(_) => "SubagentStart", + Self::SubagentStop(_) => "SubagentStop", } } } @@ -567,6 +667,16 @@ pub trait SessionHooks: Send + Sync + 'static { .await .map(HookOutput::AgentStop) .unwrap_or(HookOutput::None), + HookEvent::SubagentStart { input, ctx } => self + .on_subagent_start(input, ctx) + .await + .map(HookOutput::SubagentStart) + .unwrap_or(HookOutput::None), + HookEvent::SubagentStop { input, ctx } => self + .on_subagent_stop(input, ctx) + .await + .map(HookOutput::SubagentStop) + .unwrap_or(HookOutput::None), } } @@ -673,6 +783,25 @@ pub trait SessionHooks: Send + Sync + 'static { ) -> Option { None } + + /// Called before a subagent runs. Return context to prepend to its prompt. + async fn on_subagent_start( + &self, + _input: SubagentStartInput, + _ctx: HookContext, + ) -> Option { + None + } + + /// Called when a subagent stops. Return a block decision with a reason + /// to continue it, or a replacement for its final response. + async fn on_subagent_stop( + &self, + _input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + None + } } /// Dispatches a `hooks.invoke` request to [`SessionHooks::on_hook`]. @@ -731,6 +860,14 @@ pub(crate) async fn dispatch_hook( let input: AgentStopInput = serde_json::from_value(raw_input)?; HookEvent::AgentStop { input, ctx } } + "subagentStart" => { + let input: SubagentStartInput = serde_json::from_value(raw_input)?; + HookEvent::SubagentStart { input, ctx } + } + "subagentStop" => { + let input: SubagentStopInput = serde_json::from_value(raw_input)?; + HookEvent::SubagentStop { input, ctx } + } _ => { tracing::warn!( hook_type = hook_type, @@ -770,6 +907,8 @@ pub(crate) async fn dispatch_hook( ("sessionEnd", HookOutput::SessionEnd(o)) => Some(serde_json::to_value(o)?), ("errorOccurred", HookOutput::ErrorOccurred(o)) => Some(serde_json::to_value(o)?), ("agentStop", HookOutput::AgentStop(o)) => Some(serde_json::to_value(o)?), + ("subagentStart", HookOutput::SubagentStart(o)) => Some(serde_json::to_value(o)?), + ("subagentStop", HookOutput::SubagentStop(o)) => Some(serde_json::to_value(o)?), _ => { tracing::warn!( hook_type = hook_type, @@ -785,400 +924,4 @@ pub(crate) async fn dispatch_hook( } #[cfg(test)] -mod tests { - use super::*; - - struct TestHooks; - - #[async_trait] - impl SessionHooks for TestHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::PreToolUse { input, .. } => { - if input.tool_name == "dangerous_tool" { - HookOutput::PreToolUse(PreToolUseOutput { - permission_decision: Some("deny".to_string()), - permission_decision_reason: Some("blocked by policy".to_string()), - ..Default::default() - }) - } else { - HookOutput::None - } - } - HookEvent::UserPromptSubmitted { input, .. } => { - HookOutput::UserPromptSubmitted(UserPromptSubmittedOutput { - modified_prompt: Some(format!("[prefixed] {}", input.prompt)), - ..Default::default() - }) - } - HookEvent::UserPromptTransformed { input, .. } => { - HookOutput::UserPromptTransformed(UserPromptTransformedOutput { - modified_transformed_prompt: Some(format!( - "[transformed] {}", - input.transformed_prompt - )), - }) - } - _ => HookOutput::None, - } - } - } - - #[tokio::test] - async fn dispatch_pre_tool_use_deny() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "dangerous_tool", - "toolArgs": {} - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - let output = &result["output"]; - assert_eq!(output["permissionDecision"], "deny"); - assert_eq!(output["permissionDecisionReason"], "blocked by policy"); - } - - #[tokio::test] - async fn dispatch_pre_tool_use_passthrough() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "safe_tool", - "toolArgs": {"key": "value"} - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - // No hook registered for this tool — output should be empty object - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_user_prompt_submitted() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "prompt": "hello world" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "userPromptSubmitted", - input, - ) - .await - .unwrap(); - assert_eq!(result["output"]["modifiedPrompt"], "[prefixed] hello world"); - } - - #[tokio::test] - async fn dispatch_user_prompt_transformed() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "prompt": "hello world", - "transformedPrompt": "now\nhello world" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "userPromptTransformed", - input, - ) - .await - .unwrap(); - assert_eq!( - result["output"]["modifiedTransformedPrompt"], - "[transformed] now\nhello world" - ); - } - - #[tokio::test] - async fn dispatch_unregistered_hook_returns_empty() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "reason": "complete" - }); - // TestHooks doesn't handle SessionEnd - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionEnd", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_unknown_hook_type() { - let hooks = TestHooks; - let input = serde_json::json!({}); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "unknownHook", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_mismatched_output_returns_empty() { - struct MismatchHooks; - #[async_trait] - impl SessionHooks for MismatchHooks { - async fn on_hook(&self, _event: HookEvent) -> HookOutput { - // Always return SessionEnd output regardless of event type - HookOutput::SessionEnd(SessionEndOutput { - session_summary: Some("oops".to_string()), - ..Default::default() - }) - } - } - - let hooks = MismatchHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {} - }); - // preToolUse event gets a SessionEnd output — should be treated as empty - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_default() { - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {}, - "toolResult": "success" - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "postToolUse", input) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_default() { - // No handler override — should return an empty output object. - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {"key": "value"}, - "error": "boom" - }); - let result = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap(); - assert_eq!(result["output"], serde_json::json!({})); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_returns_additional_context() { - struct FailureHooks; - #[async_trait] - impl SessionHooks for FailureHooks { - async fn on_post_tool_use_failure( - &self, - input: PostToolUseFailureInput, - _ctx: HookContext, - ) -> Option { - assert_eq!(input.session_id, "sess-1"); - assert_eq!(input.tool_name, "some_tool"); - assert_eq!(input.error, "boom"); - assert_eq!(input.working_directory, PathBuf::from("/tmp")); - Some(PostToolUseFailureOutput { - additional_context: Some(format!( - "tool {} failed: {}", - input.tool_name, input.error - )), - }) - } - } - - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {}, - "error": "boom" - }); - let result = dispatch_hook( - &FailureHooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap(); - assert_eq!( - result["output"]["additionalContext"], - "tool some_tool failed: boom" - ); - } - - #[tokio::test] - async fn dispatch_post_tool_use_failure_invalid_input_errors() { - // Missing required `error` field — dispatcher should surface the - // deserialization error rather than dispatching with empty input. - let hooks = TestHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "toolName": "some_tool", - "toolArgs": {} - }); - let err = dispatch_hook( - &hooks, - &SessionId::new("sess-1"), - "postToolUseFailure", - input, - ) - .await - .unwrap_err(); - let msg = err.to_string().to_ascii_lowercase(); - assert!( - msg.contains("error") || msg.contains("missing field"), - "unexpected error: {msg}" - ); - } - - #[tokio::test] - async fn dispatch_session_start() { - struct StartHooks; - #[async_trait] - impl SessionHooks for StartHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::SessionStart { .. } => { - HookOutput::SessionStart(SessionStartOutput { - additional_context: Some("extra context".to_string()), - ..Default::default() - }) - } - _ => HookOutput::None, - } - } - } - - let hooks = StartHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "source": "new" - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionStart", input) - .await - .unwrap(); - assert_eq!(result["output"]["additionalContext"], "extra context"); - } - - #[tokio::test] - async fn dispatch_error_occurred() { - struct ErrorHooks; - #[async_trait] - impl SessionHooks for ErrorHooks { - async fn on_hook(&self, event: HookEvent) -> HookOutput { - match event { - HookEvent::ErrorOccurred { .. } => { - HookOutput::ErrorOccurred(ErrorOccurredOutput { - error_handling: Some("retry".to_string()), - retry_count: Some(3), - ..Default::default() - }) - } - _ => HookOutput::None, - } - } - } - - let hooks = ErrorHooks; - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "error": "model timeout", - "errorContext": "model_call", - "recoverable": true - }); - let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "errorOccurred", input) - .await - .unwrap(); - assert_eq!(result["output"]["errorHandling"], "retry"); - assert_eq!(result["output"]["retryCount"], 3); - } - - #[tokio::test] - async fn dispatch_agent_stop_block() { - struct AgentStopHooks; - #[async_trait] - impl SessionHooks for AgentStopHooks { - async fn on_agent_stop( - &self, - input: AgentStopInput, - ctx: HookContext, - ) -> Option { - assert_eq!(ctx.session_id, SessionId::new("sess-1")); - assert_eq!(input.session_id, "sess-1"); - assert_eq!(input.stop_reason.as_deref(), Some("end_turn")); - assert_eq!( - input.transcript_path, - Some(PathBuf::from("/tmp/transcript.jsonl")) - ); - assert_eq!(input.stop_hook_active, Some(true)); - Some(AgentStopOutput { - decision: Some("block".to_string()), - reason: Some("finish the remaining work".to_string()), - }) - } - } - - let input = serde_json::json!({ - "sessionId": "sess-1", - "timestamp": 1234567890, - "cwd": "/tmp", - "stopReason": "end_turn", - "transcriptPath": "/tmp/transcript.jsonl", - "stop_hook_active": true - }); - let result = dispatch_hook( - &AgentStopHooks, - &SessionId::new("sess-1"), - "agentStop", - input, - ) - .await - .unwrap(); - - assert_eq!(result["output"]["decision"], "block"); - assert_eq!(result["output"]["reason"], "finish the remaining work"); - } -} +mod tests; diff --git a/rust/src/hooks/tests.rs b/rust/src/hooks/tests.rs new file mode 100644 index 0000000000..e8a8179c41 --- /dev/null +++ b/rust/src/hooks/tests.rs @@ -0,0 +1,530 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +struct TestHooks; + +#[async_trait] +impl SessionHooks for TestHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::PreToolUse { input, .. } => { + if input.tool_name == "dangerous_tool" { + HookOutput::PreToolUse(PreToolUseOutput { + permission_decision: Some("deny".to_string()), + permission_decision_reason: Some("blocked by policy".to_string()), + ..Default::default() + }) + } else { + HookOutput::None + } + } + HookEvent::UserPromptSubmitted { input, .. } => { + HookOutput::UserPromptSubmitted(UserPromptSubmittedOutput { + modified_prompt: Some(format!("[prefixed] {}", input.prompt)), + ..Default::default() + }) + } + HookEvent::UserPromptTransformed { input, .. } => { + HookOutput::UserPromptTransformed(UserPromptTransformedOutput { + modified_transformed_prompt: Some(format!( + "[transformed] {}", + input.transformed_prompt + )), + }) + } + _ => HookOutput::None, + } + } +} + +#[tokio::test] +async fn dispatch_pre_tool_use_deny() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "dangerous_tool", + "toolArgs": {} + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + let output = &result["output"]; + assert_eq!(output["permissionDecision"], "deny"); + assert_eq!(output["permissionDecisionReason"], "blocked by policy"); +} + +#[tokio::test] +async fn dispatch_pre_tool_use_passthrough() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "safe_tool", + "toolArgs": {"key": "value"} + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + // No hook registered for this tool — output should be empty object + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_user_prompt_submitted() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "prompt": "hello world" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "userPromptSubmitted", + input, + ) + .await + .unwrap(); + assert_eq!(result["output"]["modifiedPrompt"], "[prefixed] hello world"); +} + +#[tokio::test] +async fn dispatch_user_prompt_transformed() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "prompt": "hello world", + "transformedPrompt": "now\nhello world" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "userPromptTransformed", + input, + ) + .await + .unwrap(); + assert_eq!( + result["output"]["modifiedTransformedPrompt"], + "[transformed] now\nhello world" + ); +} + +#[tokio::test] +async fn dispatch_unregistered_hook_returns_empty() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "reason": "complete" + }); + // TestHooks doesn't handle SessionEnd + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionEnd", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_unknown_hook_type() { + let hooks = TestHooks; + let input = serde_json::json!({}); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "unknownHook", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_subagent_hooks_with_typed_input_and_output() { + struct SubagentHooks; + #[async_trait] + impl SessionHooks for SubagentHooks { + async fn on_subagent_start( + &self, + input: SubagentStartInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.timestamp, 1234567890.0); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + assert_eq!( + input.transcript_path, + PathBuf::from("/tmp/transcript.jsonl") + ); + assert_eq!(input.agent_name, "explore"); + assert_eq!(input.agent_display_name.as_deref(), Some("Explore Agent")); + assert_eq!(input.agent_description.as_deref(), Some("Read code")); + Some(SubagentStartOutput { + additional_context: Some("Follow the file".to_string()), + }) + } + + async fn on_subagent_stop( + &self, + input: SubagentStopInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.timestamp, 1234567891.0); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + assert_eq!( + input.transcript_path, + PathBuf::from("/tmp/transcript.jsonl") + ); + assert_eq!(input.agent_name, "explore"); + assert_eq!(input.agent_type, "explore"); + assert_eq!(input.agent_id.as_deref(), Some("read-file")); + assert_eq!(input.agent_display_name.as_deref(), Some("Explore Agent")); + assert_eq!(input.agent_description.as_deref(), Some("Read code")); + assert_eq!(input.stop_reason, "end_turn"); + assert_eq!(input.response, "original answer"); + Some(SubagentStopOutput { + modified_response: Some("rewritten answer".to_string()), + ..Default::default() + }) + } + } + + let common = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "explore", + "agentDisplayName": "Explore Agent", + "agentDescription": "Read code" + }); + let start = dispatch_hook( + &SubagentHooks, + &SessionId::new("sess-1"), + "subagentStart", + common.clone(), + ) + .await + .unwrap(); + assert_eq!( + start, + serde_json::json!({ "output": { "additionalContext": "Follow the file" } }) + ); + + let mut stop = common; + let object = stop.as_object_mut().unwrap(); + object.insert("timestamp".to_string(), serde_json::json!(1234567891)); + object.insert("agentType".to_string(), serde_json::json!("explore")); + object.insert("agentId".to_string(), serde_json::json!("read-file")); + object.insert("stopReason".to_string(), serde_json::json!("end_turn")); + object.insert("response".to_string(), serde_json::json!("original answer")); + let stopped = dispatch_hook( + &SubagentHooks, + &SessionId::new("sess-1"), + "subagentStop", + stop, + ) + .await + .unwrap(); + assert_eq!( + stopped, + serde_json::json!({ "output": { "modifiedResponse": "rewritten answer" } }) + ); +} + +#[tokio::test] +async fn dispatch_subagent_stop_block_output() { + struct BlockHook; + #[async_trait] + impl SessionHooks for BlockHook { + async fn on_subagent_stop( + &self, + _input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + Some(SubagentStopOutput { + decision: Some("block".to_string()), + reason: Some("Keep researching".to_string()), + ..Default::default() + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "explore", + "agentType": "explore", + "stopReason": "end_turn", + "response": "draft answer" + }); + let result = dispatch_hook(&BlockHook, &SessionId::new("sess-1"), "subagentStop", input) + .await + .unwrap(); + assert_eq!( + result, + serde_json::json!({ "output": { "decision": "block", "reason": "Keep researching" } }) + ); +} + +#[tokio::test] +async fn dispatch_mismatched_output_returns_empty() { + struct MismatchHooks; + #[async_trait] + impl SessionHooks for MismatchHooks { + async fn on_hook(&self, _event: HookEvent) -> HookOutput { + // Always return SessionEnd output regardless of event type + HookOutput::SessionEnd(SessionEndOutput { + session_summary: Some("oops".to_string()), + ..Default::default() + }) + } + } + + let hooks = MismatchHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {} + }); + // preToolUse event gets a SessionEnd output — should be treated as empty + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "preToolUse", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_default() { + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {}, + "toolResult": "success" + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "postToolUse", input) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_default() { + // No handler override — should return an empty output object. + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {"key": "value"}, + "error": "boom" + }); + let result = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap(); + assert_eq!(result["output"], serde_json::json!({})); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_returns_additional_context() { + struct FailureHooks; + #[async_trait] + impl SessionHooks for FailureHooks { + async fn on_post_tool_use_failure( + &self, + input: PostToolUseFailureInput, + _ctx: HookContext, + ) -> Option { + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.tool_name, "some_tool"); + assert_eq!(input.error, "boom"); + assert_eq!(input.working_directory, PathBuf::from("/tmp")); + Some(PostToolUseFailureOutput { + additional_context: Some(format!( + "tool {} failed: {}", + input.tool_name, input.error + )), + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {}, + "error": "boom" + }); + let result = dispatch_hook( + &FailureHooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap(); + assert_eq!( + result["output"]["additionalContext"], + "tool some_tool failed: boom" + ); +} + +#[tokio::test] +async fn dispatch_post_tool_use_failure_invalid_input_errors() { + // Missing required `error` field — dispatcher should surface the + // deserialization error rather than dispatching with empty input. + let hooks = TestHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "toolName": "some_tool", + "toolArgs": {} + }); + let err = dispatch_hook( + &hooks, + &SessionId::new("sess-1"), + "postToolUseFailure", + input, + ) + .await + .unwrap_err(); + let msg = err.to_string().to_ascii_lowercase(); + assert!( + msg.contains("error") || msg.contains("missing field"), + "unexpected error: {msg}" + ); +} + +#[tokio::test] +async fn dispatch_session_start() { + struct StartHooks; + #[async_trait] + impl SessionHooks for StartHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::SessionStart { .. } => HookOutput::SessionStart(SessionStartOutput { + additional_context: Some("extra context".to_string()), + ..Default::default() + }), + _ => HookOutput::None, + } + } + } + + let hooks = StartHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "source": "new" + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "sessionStart", input) + .await + .unwrap(); + assert_eq!(result["output"]["additionalContext"], "extra context"); +} + +#[tokio::test] +async fn dispatch_error_occurred() { + struct ErrorHooks; + #[async_trait] + impl SessionHooks for ErrorHooks { + async fn on_hook(&self, event: HookEvent) -> HookOutput { + match event { + HookEvent::ErrorOccurred { .. } => HookOutput::ErrorOccurred(ErrorOccurredOutput { + error_handling: Some("retry".to_string()), + retry_count: Some(3), + ..Default::default() + }), + _ => HookOutput::None, + } + } + } + + let hooks = ErrorHooks; + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "error": "model timeout", + "errorContext": "model_call", + "recoverable": true + }); + let result = dispatch_hook(&hooks, &SessionId::new("sess-1"), "errorOccurred", input) + .await + .unwrap(); + assert_eq!(result["output"]["errorHandling"], "retry"); + assert_eq!(result["output"]["retryCount"], 3); +} + +#[tokio::test] +async fn dispatch_agent_stop_block() { + struct AgentStopHooks; + #[async_trait] + impl SessionHooks for AgentStopHooks { + async fn on_agent_stop( + &self, + input: AgentStopInput, + ctx: HookContext, + ) -> Option { + assert_eq!(ctx.session_id, SessionId::new("sess-1")); + assert_eq!(input.session_id, "sess-1"); + assert_eq!(input.stop_reason.as_deref(), Some("end_turn")); + assert_eq!( + input.transcript_path, + Some(PathBuf::from("/tmp/transcript.jsonl")) + ); + assert_eq!(input.stop_hook_active, Some(true)); + Some(AgentStopOutput { + decision: Some("block".to_string()), + reason: Some("finish the remaining work".to_string()), + }) + } + } + + let input = serde_json::json!({ + "sessionId": "sess-1", + "timestamp": 1234567890, + "cwd": "/tmp", + "stopReason": "end_turn", + "transcriptPath": "/tmp/transcript.jsonl", + "stop_hook_active": true + }); + let result = dispatch_hook( + &AgentStopHooks, + &SessionId::new("sess-1"), + "agentStop", + input, + ) + .await + .unwrap(); + + assert_eq!(result["output"]["decision"], "block"); + assert_eq!(result["output"]["reason"], "finish the remaining work"); +} diff --git a/rust/src/installation_confirmation.rs b/rust/src/installation_confirmation.rs index 9e0ef39f29..ccb9b46b01 100644 --- a/rust/src/installation_confirmation.rs +++ b/rust/src/installation_confirmation.rs @@ -2,14 +2,12 @@ //! Connection-global human confirmation for experimental installation operations. -use std::collections::HashMap; -use std::collections::hash_map::Entry; use std::panic::AssertUnwindSafe; use std::sync::{Arc, OnceLock, Weak}; use async_trait::async_trait; use futures_util::FutureExt; -use parking_lot::{Mutex, RwLock}; +use parking_lot::RwLock; use tokio_util::sync::CancellationToken; use tracing::warn; @@ -22,7 +20,6 @@ use crate::{ }; pub(crate) const CONFIRM_METHOD: &str = "installations.confirm"; -const REQUEST_CANCELLED: i32 = -32800; /// The cancellation lifetime of one confirmation. /// @@ -66,63 +63,6 @@ pub trait InstallationConfirmationHandler: Send + Sync + 'static { ) -> Result; } -/// Registered synchronously by the transport before forwarding each confirmation. -/// This preserves request/cancellation ordering across the router's separate queues. -#[derive(Default)] -pub(crate) struct ConfirmationRequests { - pending: Mutex>>, -} - -impl ConfirmationRequests { - pub(crate) fn register(&self, id: u64) -> bool { - let mut pending = self.pending.lock(); - match pending.entry(id) { - Entry::Vacant(entry) => { - entry.insert(Arc::new(CancellationToken::new())); - true - } - Entry::Occupied(_) => false, - } - } - - pub(crate) fn cancel(&self, id: u64) { - if let Some(token) = self.pending.lock().get(&id) { - token.cancel(); - } - } - - pub(crate) fn clear(&self) { - self.pending.lock().clear(); - } - - fn claim(self: &Arc, id: u64) -> Option { - let cancellation = self.pending.lock().get(&id)?.clone(); - Some(PendingConfirmation { - requests: self.clone(), - id, - cancellation, - }) - } -} - -struct PendingConfirmation { - requests: Arc, - id: u64, - cancellation: Arc, -} - -impl Drop for PendingConfirmation { - fn drop(&mut self) { - let mut pending = self.requests.pending.lock(); - if pending - .get(&self.id) - .is_some_and(|token| Arc::ptr_eq(token, &self.cancellation)) - { - pending.remove(&self.id); - } - } -} - pub(crate) struct InstallationConfirmationDispatcher { handler: RwLock>>, client: OnceLock>, @@ -140,6 +80,7 @@ impl InstallationConfirmationDispatcher { let _ = self.client.set(client); } + #[cfg(any(feature = "runtime", test, feature = "test-support"))] pub(crate) fn set_handler(&self, handler: Option>) { *self.handler.write() = handler; } @@ -152,11 +93,11 @@ impl InstallationConfirmationDispatcher { let Some(client) = self.client.get().and_then(Weak::upgrade) else { return; }; - let Some(pending) = client.rpc.confirmation_requests.claim(request.id) else { + let Some(pending) = client.rpc.cancellable_requests.claim(request.id) else { warn!("confirmation request retired before dispatch"); return; }; - let request_cancelled = pending.cancellation.as_ref().clone(); + let request_cancelled = pending.cancellation().clone(); let connection_closed = client.rpc.connection_closed_token(); let context = InstallationConfirmationContext { cancellation: connection_closed.child_token(), @@ -169,7 +110,7 @@ impl InstallationConfirmationDispatcher { _ = connection_closed.cancelled() => return, _ = request_cancelled.cancelled() => { context.cancellation.cancel(); - Err((REQUEST_CANCELLED, "Installation confirmation request cancelled")) + Err((error_codes::REQUEST_CANCELLED, "Installation confirmation request cancelled")) } outcome = Self::handle(handler, request.params, context.clone()) => outcome, }; @@ -179,7 +120,7 @@ impl InstallationConfirmationDispatcher { let outcome = if request_cancelled.is_cancelled() { context.cancellation.cancel(); Err(( - REQUEST_CANCELLED, + error_codes::REQUEST_CANCELLED, "Installation confirmation request cancelled", )) } else { diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 32cd242b77..2f69da5ba9 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::hash_map::Entry; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Instant; @@ -98,6 +99,8 @@ pub mod error_codes { /// Internal server error (-32603). #[allow(dead_code, reason = "standard JSON-RPC code, reserved for future use")] pub const INTERNAL_ERROR: i32 = -32603; + /// Request cancelled by the peer's `$/cancelRequest` (-32800). + pub const REQUEST_CANCELLED: i32 = -32800; } /// A JSON-RPC 2.0 notification (no `id`, no response expected). @@ -276,6 +279,85 @@ struct WriteCommand { ack: oneshot::Sender>, } +/// Inbound requests that honor `$/cancelRequest`. +/// +/// The read loop registers each one synchronously before forwarding it. This +/// preserves request/cancellation ordering across the router's separate queues. +#[derive(Default)] +pub(crate) struct CancellableRequests { + pending: Mutex>>, +} + +impl CancellableRequests { + fn honors_cancellation(method: &str) -> bool { + use crate::generated::api_types::rpc_methods; + + matches!( + method, + crate::installation_confirmation::CONFIRM_METHOD + | rpc_methods::SKILLPROVIDER_LIST + | rpc_methods::SKILLPROVIDER_READ + ) + } + + fn register(&self, id: u64) -> bool { + let mut pending = self.pending.lock(); + match pending.entry(id) { + Entry::Vacant(entry) => { + entry.insert(Arc::new(CancellationToken::new())); + true + } + Entry::Occupied(_) => false, + } + } + + fn cancel(&self, id: u64) { + if let Some(token) = self.pending.lock().get(&id) { + token.cancel(); + } + } + + fn clear(&self) { + self.pending.lock().clear(); + } + + /// Take ownership of a registered request's cancellation until the + /// returned guard drops, or `None` if the connection already retired it. + pub(crate) fn claim(self: &Arc, id: u64) -> Option { + let cancellation = self.pending.lock().get(&id)?.clone(); + Some(PendingCancellation { + requests: self.clone(), + id, + cancellation, + }) + } +} + +pub(crate) struct PendingCancellation { + requests: Arc, + id: u64, + cancellation: Arc, +} + +impl PendingCancellation { + /// Cancelled when the runtime sends `$/cancelRequest` for this request. + pub(crate) fn cancellation(&self) -> &CancellationToken { + &self.cancellation + } +} + +impl Drop for PendingCancellation { + fn drop(&mut self) { + let mut pending = self.requests.pending.lock(); + if pending + .get(&self.id) + .is_some_and(|token| Arc::ptr_eq(token, &self.cancellation)) + { + pending.remove(&self.id); + } + } +} + /// Low-level JSON-RPC 2.0 client over Content-Length-framed streams. /// /// # Cancel safety @@ -300,7 +382,7 @@ pub struct JsonRpcClient { request_tx: mpsc::UnboundedSender, request_handlers: RequestHandlers, connection_closed: CancellationToken, - pub(crate) confirmation_requests: Arc, + pub(crate) cancellable_requests: Arc, read_task: Mutex>>, write_task: Mutex>>, } @@ -342,9 +424,7 @@ impl JsonRpcClient { request_tx, request_handlers: Arc::new(RwLock::new(HashMap::new())), connection_closed: CancellationToken::new(), - confirmation_requests: Arc::new( - crate::installation_confirmation::ConfirmationRequests::default(), - ), + cancellable_requests: Arc::new(CancellableRequests::default()), read_task: Mutex::new(None), write_task: Mutex::new(Some(write_task)), }; @@ -353,7 +433,7 @@ impl JsonRpcClient { let notification_tx_clone = client.notification_tx.clone(); let request_tx_clone = client.request_tx.clone(); let connection_closed = client.connection_closed.clone(); - let confirmation_requests = client.confirmation_requests.clone(); + let cancellable_requests = client.cancellable_requests.clone(); let request_handlers = client.request_handlers.clone(); let write_tx = client.write_tx.clone(); let reader_span = tracing::error_span!("jsonrpc_read_loop"); @@ -367,7 +447,7 @@ impl JsonRpcClient { request_tx_clone, request_handlers, write_tx, - (connection_closed, confirmation_requests), + (connection_closed, cancellable_requests), ) .await; } @@ -380,7 +460,7 @@ impl JsonRpcClient { pub(crate) fn force_close(&self) { self.connection_closed.cancel(); - self.confirmation_requests.clear(); + self.cancellable_requests.clear(); let handlers = std::mem::take(&mut *self.request_handlers.write()); drop(handlers); if let Some(task) = self.read_task.lock().take() { @@ -464,13 +544,10 @@ impl JsonRpcClient { request_tx: mpsc::UnboundedSender, request_handlers: RequestHandlers, write_tx: mpsc::UnboundedSender, - connection: ( - CancellationToken, - Arc, - ), + connection: (CancellationToken, Arc), ) { let mut reader = BufReader::new(reader); - let (connection_closed, confirmation_requests) = connection; + let (connection_closed, cancellable_requests) = connection; loop { match Self::read_message(&mut reader).await { @@ -537,7 +614,7 @@ impl JsonRpcClient { .and_then(|params| params.get("id")) .and_then(Value::as_u64) { - confirmation_requests.cancel(id); + cancellable_requests.cancel(id); } else { warn!("invalid numeric request cancellation"); } @@ -552,10 +629,10 @@ impl JsonRpcClient { let _ = notifications.0.send(notification); } JsonRpcMessage::Request(request) => { - if request.method == crate::installation_confirmation::CONFIRM_METHOD - && !confirmation_requests.register(request.id) + if CancellableRequests::honors_cancellation(&request.method) + && !cancellable_requests.register(request.id) { - warn!("duplicate pending installation confirmation request ID"); + warn!(method = %request.method, "duplicate pending cancellable request ID"); break; } let handler = request_handlers.read().get(&request.method).cloned(); @@ -608,7 +685,7 @@ impl JsonRpcClient { } } connection_closed.cancel(); - confirmation_requests.clear(); + cancellable_requests.clear(); // A handler may own the last Client clone, whose drop closes the RPC. // Release the registry lock before dropping those captured values. let handlers = std::mem::take(&mut *request_handlers.write()); @@ -889,161 +966,4 @@ impl Drop for PendingGuard<'_> { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn listener_negotiation_logs_do_not_echo_remote_errors() { - let message = "request rejected: token=listener-secret-sentinel"; - for method in ["host.getConfiguration", "host.ready"] { - assert_eq!( - remote_error_log_message(method, message), - "listener negotiation request rejected" - ); - } - assert_eq!(remote_error_log_message("ping", message), message); - } - - #[test] - fn deserialize_notification() { - let json = r#"{"jsonrpc":"2.0","method":"session.event","params":{"id":"e1"}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - assert!(matches!(msg, JsonRpcMessage::Notification(n) if n.method == "session.event")); - } - - #[test] - fn deserialize_request() { - let json = - r#"{"jsonrpc":"2.0","id":5,"method":"permission.request","params":{"kind":"shell"}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - assert!( - matches!(msg, JsonRpcMessage::Request(r) if r.id == 5 && r.method == "permission.request") - ); - } - - #[test] - fn deserialize_response_with_result() { - let json = r#"{"jsonrpc":"2.0","id":3,"result":{"ok":true}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - assert!(matches!(msg, JsonRpcMessage::Response(r) if r.id == 3 && !r.is_error())); - } - - #[test] - fn deserialize_error_response() { - let json = r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"Invalid Request","data":{"nested":[1,{"reason":"invalid"}]}}}"#; - let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); - match msg { - JsonRpcMessage::Response(r) => { - assert!(r.is_error()); - let err = r.error.unwrap(); - assert_eq!(err.code, -32600); - assert_eq!(err.message, "Invalid Request"); - assert_eq!( - err.data, - Some(serde_json::json!({"nested": [1, {"reason": "invalid"}]})) - ); - } - other => panic!("expected Response, got {other:?}"), - } - } - - #[test] - fn deserialize_rejects_non_object() { - let result = serde_json::from_str::(r#""not an object""#); - assert!(result.is_err()); - } - - #[test] - fn deserialize_preserves_optional_payloads() { - for payload in [ - None, - Some(Value::Null), - Some(serde_json::json!(false)), - Some(serde_json::json!(42)), - Some(serde_json::json!("text")), - Some(serde_json::json!([{"nested": [1, null, true]}])), - Some(serde_json::json!({"rows": [{"content": "result"}]})), - ] { - for mut envelope in [ - serde_json::json!({"jsonrpc": "2.0", "method": "notify"}), - serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "request"}), - serde_json::json!({"jsonrpc": "2.0", "id": 1}), - ] { - let (payload_key, ignored_key) = if envelope.get("method").is_some() { - ("params", "result") - } else { - ("result", "params") - }; - envelope[ignored_key] = serde_json::json!({"ignored": "opposite payload"}); - if let Some(payload) = &payload { - envelope[payload_key] = payload.clone(); - } - let actual = match serde_json::from_value::(envelope).unwrap() { - JsonRpcMessage::Request(request) => request.params, - JsonRpcMessage::Response(response) => response.result, - JsonRpcMessage::Notification(notification) => notification.params, - }; - assert_eq!(actual, payload.clone().filter(|value| !value.is_null())); - } - } - } - - #[test] - fn deserialize_rejects_invalid_metadata() { - for json in [ - r#"{"jsonrpc":null,"method":"notify","params":{"nested":[1]}}"#, - r#"{"jsonrpc":"2.0","method":42,"params":{"nested":[1]}}"#, - r#"{"jsonrpc":"2.0","id":null,"result":{}}"#, - r#"{"jsonrpc":"2.0","id":"1","result":{}}"#, - r#"{"jsonrpc":"2.0","id":-1,"result":{}}"#, - r#"{"jsonrpc":"2.0","id":1,"method":null,"params":{}}"#, - r#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":"bad","message":"error"}}"#, - ] { - assert!( - serde_json::from_str::(json).is_err(), - "{json}" - ); - } - } - - #[test] - fn request_new_sets_version() { - let req = JsonRpcRequest::new(42, "test.method", None); - assert_eq!(req.jsonrpc, "2.0"); - assert_eq!(req.id, 42); - assert_eq!(req.method, "test.method"); - assert!(req.params.is_none()); - } - - #[test] - fn request_serializes_camel_case() { - let req = JsonRpcRequest::new(1, "ping", Some(serde_json::json!({}))); - let json = serde_json::to_string(&req).unwrap(); - assert!(json.contains(r#""jsonrpc":"2.0""#)); - assert!(json.contains(r#""id":1"#)); - assert!(json.contains(r#""method":"ping""#)); - } - - #[test] - fn notification_without_params_omits_field() { - let n = JsonRpcNotification { - jsonrpc: "2.0".into(), - method: "ping".into(), - params: None, - }; - let json = serde_json::to_string(&n).unwrap(); - assert!(!json.contains("params")); - } - - #[test] - fn response_without_error_omits_field() { - let r = JsonRpcResponse { - jsonrpc: "2.0".into(), - id: 1, - result: Some(serde_json::json!(true)), - error: None, - }; - let json = serde_json::to_string(&r).unwrap(); - assert!(!json.contains("error")); - } -} +mod tests; diff --git a/rust/src/jsonrpc/tests.rs b/rust/src/jsonrpc/tests.rs new file mode 100644 index 0000000000..0b7f154c83 --- /dev/null +++ b/rust/src/jsonrpc/tests.rs @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn listener_negotiation_logs_do_not_echo_remote_errors() { + let message = "request rejected: token=listener-secret-sentinel"; + for method in ["host.getConfiguration", "host.ready"] { + assert_eq!( + remote_error_log_message(method, message), + "listener negotiation request rejected" + ); + } + assert_eq!(remote_error_log_message("ping", message), message); +} + +#[test] +fn deserialize_notification() { + let json = r#"{"jsonrpc":"2.0","method":"session.event","params":{"id":"e1"}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + assert!(matches!(msg, JsonRpcMessage::Notification(n) if n.method == "session.event")); +} + +#[test] +fn deserialize_request() { + let json = + r#"{"jsonrpc":"2.0","id":5,"method":"permission.request","params":{"kind":"shell"}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + assert!( + matches!(msg, JsonRpcMessage::Request(r) if r.id == 5 && r.method == "permission.request") + ); +} + +#[test] +fn deserialize_response_with_result() { + let json = r#"{"jsonrpc":"2.0","id":3,"result":{"ok":true}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + assert!(matches!(msg, JsonRpcMessage::Response(r) if r.id == 3 && !r.is_error())); +} + +#[test] +fn deserialize_error_response() { + let json = r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"Invalid Request","data":{"nested":[1,{"reason":"invalid"}]}}}"#; + let msg: JsonRpcMessage = serde_json::from_str(json).unwrap(); + match msg { + JsonRpcMessage::Response(r) => { + assert!(r.is_error()); + let err = r.error.unwrap(); + assert_eq!(err.code, -32600); + assert_eq!(err.message, "Invalid Request"); + assert_eq!( + err.data, + Some(serde_json::json!({"nested": [1, {"reason": "invalid"}]})) + ); + } + other => panic!("expected Response, got {other:?}"), + } +} + +#[test] +fn deserialize_rejects_non_object() { + let result = serde_json::from_str::(r#""not an object""#); + assert!(result.is_err()); +} + +#[test] +fn deserialize_preserves_optional_payloads() { + for payload in [ + None, + Some(Value::Null), + Some(serde_json::json!(false)), + Some(serde_json::json!(42)), + Some(serde_json::json!("text")), + Some(serde_json::json!([{"nested": [1, null, true]}])), + Some(serde_json::json!({"rows": [{"content": "result"}]})), + ] { + for mut envelope in [ + serde_json::json!({"jsonrpc": "2.0", "method": "notify"}), + serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "request"}), + serde_json::json!({"jsonrpc": "2.0", "id": 1}), + ] { + let (payload_key, ignored_key) = if envelope.get("method").is_some() { + ("params", "result") + } else { + ("result", "params") + }; + envelope[ignored_key] = serde_json::json!({"ignored": "opposite payload"}); + if let Some(payload) = &payload { + envelope[payload_key] = payload.clone(); + } + let actual = match serde_json::from_value::(envelope).unwrap() { + JsonRpcMessage::Request(request) => request.params, + JsonRpcMessage::Response(response) => response.result, + JsonRpcMessage::Notification(notification) => notification.params, + }; + assert_eq!(actual, payload.clone().filter(|value| !value.is_null())); + } + } +} + +#[test] +fn deserialize_rejects_invalid_metadata() { + for json in [ + r#"{"jsonrpc":null,"method":"notify","params":{"nested":[1]}}"#, + r#"{"jsonrpc":"2.0","method":42,"params":{"nested":[1]}}"#, + r#"{"jsonrpc":"2.0","id":null,"result":{}}"#, + r#"{"jsonrpc":"2.0","id":"1","result":{}}"#, + r#"{"jsonrpc":"2.0","id":-1,"result":{}}"#, + r#"{"jsonrpc":"2.0","id":1,"method":null,"params":{}}"#, + r#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":"bad","message":"error"}}"#, + ] { + assert!( + serde_json::from_str::(json).is_err(), + "{json}" + ); + } +} + +#[test] +fn request_new_sets_version() { + let req = JsonRpcRequest::new(42, "test.method", None); + assert_eq!(req.jsonrpc, "2.0"); + assert_eq!(req.id, 42); + assert_eq!(req.method, "test.method"); + assert!(req.params.is_none()); +} + +#[test] +fn request_serializes_camel_case() { + let req = JsonRpcRequest::new(1, "ping", Some(serde_json::json!({}))); + let json = serde_json::to_string(&req).unwrap(); + assert!(json.contains(r#""jsonrpc":"2.0""#)); + assert!(json.contains(r#""id":1"#)); + assert!(json.contains(r#""method":"ping""#)); +} + +#[test] +fn notification_without_params_omits_field() { + let n = JsonRpcNotification { + jsonrpc: "2.0".into(), + method: "ping".into(), + params: None, + }; + let json = serde_json::to_string(&n).unwrap(); + assert!(!json.contains("params")); +} + +#[test] +fn response_without_error_omits_field() { + let r = JsonRpcResponse { + jsonrpc: "2.0".into(), + id: 1, + result: Some(serde_json::json!(true)), + error: None, + }; + let json = serde_json::to_string(&r).unwrap(); + assert!(!json.contains("error")); +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index da05be1482..93790a94b8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -11,10 +11,17 @@ pub use ahp_host::{ AhpSessionResumeRequest, }; +// Outside tests, `cache_paths`'s only caller is `resolve::extracted_program`, which +// needs the build script to have extracted a CLI (`has_extracted_cli`). Without it the +// module is compiled only for its own unit tests, which exercise the pure path helpers; +// `extracted_runtime_install_dir` and `platform_cache_dir` then have no caller in the +// lib test unit (the build script reaches them through its own `#[path]` include). +#[cfg_attr(all(test, not(has_extracted_cli)), expect(dead_code))] #[cfg(all( feature = "runtime", not(feature = "bundled-cli"), - not(feature = "local-runtime") + not(feature = "local-runtime"), + any(test, has_extracted_cli) ))] mod cache_paths; /// Canvas declarations, provider callbacks, and host-side canvas RPC types. @@ -62,6 +69,8 @@ pub mod session; /// Custom session filesystem provider (virtualizable filesystem layer). pub mod session_fs; mod session_fs_dispatch; +/// Session-scoped skill provider callbacks. +pub mod skill_provider; /// Per-phase timing breakdown for [`Client::start`]. pub mod startup_timings; /// Event subscription handles returned by `subscribe()` methods. @@ -124,6 +133,7 @@ pub(crate) use jsonrpc::{ }; pub use mode::{BUILTIN_TOOLS_ISOLATED, ClientMode, ToolSet}; pub use provider_token::{BearerTokenError, BearerTokenProvider, ProviderTokenArgs}; +pub use skill_provider::{SkillProvider, SkillProviderDescriptor}; /// Re-exported JSON-RPC internals for integration tests (requires `test-support` feature). #[cfg(feature = "test-support")] @@ -1262,6 +1272,7 @@ struct ClientInner { models_cache: parking_lot::Mutex>>>, session_fs_configured: bool, session_fs_sqlite_declared: bool, + session_fs_binary_declared: bool, /// Inbound `llmInference.*` dispatcher, installed when /// [`ClientOptions::request_handler`] is set. llm_inference: OnceLock>, @@ -1436,6 +1447,10 @@ impl Client { .as_ref() .and_then(|c| c.capabilities.as_ref()) .is_some_and(|caps| caps.sqlite); + let session_fs_binary_declared = session_fs_config + .as_ref() + .and_then(|c| c.capabilities.as_ref()) + .is_some_and(|caps| caps.binary); let program = match &options.program { CliProgram::Path(path) => { info!(path = %path.display(), "using explicit copilot CLI path"); @@ -1507,6 +1522,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1542,6 +1558,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1567,6 +1584,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1636,6 +1654,7 @@ impl Client { extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, + session_fs_binary_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), @@ -1691,6 +1710,7 @@ impl Client { let capabilities = cfg.capabilities.as_ref().map(|c| { crate::generated::api_types::SessionFsSetProviderCapabilities { sqlite: Some(c.sqlite), + binary: Some(c.binary), } }); let request = crate::generated::api_types::SessionFsSetProviderRequest { @@ -1814,6 +1834,39 @@ impl Client { None, false, false, + false, + None, + None, + None, + ClientMode::default(), + None, + false, + ) + } + + /// Construct a client with declared session filesystem capabilities for + /// framed transport tests, without starting a CLI process. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_session_fs_config( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + config: SessionFsConfig, + ) -> Result { + let sqlite = config.capabilities.as_ref().is_some_and(|c| c.sqlite); + let binary = config.capabilities.as_ref().is_some_and(|c| c.binary); + Self::from_transport( + reader, + writer, + None, + None, + cwd, + None, + None, + true, + sqlite, + binary, None, None, None, @@ -1843,6 +1896,7 @@ impl Client { Some(provider), false, false, + false, None, None, None, @@ -1893,6 +1947,7 @@ impl Client { None, false, false, + false, Some(provider), None, None, @@ -1922,6 +1977,7 @@ impl Client { None, false, false, + false, None, None, token, @@ -1951,6 +2007,7 @@ impl Client { None, false, false, + false, None, Some(on_github_telemetry), None, @@ -1991,6 +2048,7 @@ impl Client { None, false, false, + false, None, None, None, @@ -2000,6 +2058,22 @@ impl Client { ) } + /// Construct a client from raw streams with a preset mode, for integration + /// tests that need mode-specific session defaults without spawning a CLI. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_mode_for_test( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + mode: ClientMode, + ) -> Result { + Self::from_transport( + reader, writer, None, None, cwd, None, None, false, false, false, None, None, None, + mode, None, false, + ) + } + #[allow(clippy::too_many_arguments)] fn from_transport( reader: impl AsyncRead + Unpin + Send + 'static, @@ -2013,6 +2087,7 @@ impl Client { >, session_fs_configured: bool, session_fs_sqlite_declared: bool, + session_fs_binary_declared: bool, on_get_trace_context: Option>, on_github_telemetry: Option, effective_connection_token: Option, @@ -2068,6 +2143,7 @@ impl Client { models_cache: parking_lot::Mutex::new(Arc::new(tokio::sync::OnceCell::new())), session_fs_configured, session_fs_sqlite_declared, + session_fs_binary_declared, llm_inference: OnceLock::new(), extension_launch_provider: extension_launch_provider.clone(), installation_confirmation: installation_confirmation.clone(), @@ -2467,32 +2543,37 @@ impl Client { /// If the callback returns an error, that error is propagated to /// this awaiter in place of the response. The callback never causes /// the read loop to crash. - pub(crate) async fn call_with_inline_callback( + /// + /// The returned future owns the request but not this `Client`, so it + /// can be spawned without keeping the connection open. + pub(crate) fn call_with_inline_callback( &self, method: &str, params: Option, inline_callback: Option, - ) -> Result { + ) -> impl std::future::Future> + Send + 'static { let session_id: Option = params .as_ref() .and_then(|p| p.get("sessionId")) .and_then(|v| v.as_str()) .map(SessionId::from); - let response = self - .inner - .rpc - .send_request_with_inline_callback(method, params, inline_callback) - .await?; - if let Some(err) = response.error { - if err.message.contains("Session not found") { - return Err(ErrorKind::Session(SessionErrorKind::NotFound( - session_id.unwrap_or_else(|| "unknown".into()), - )) - .into()); + let request = + self.inner + .rpc + .send_request_with_inline_callback(method, params, inline_callback); + async move { + let response = request.await?; + if let Some(err) = response.error { + if err.message.contains("Session not found") { + return Err(ErrorKind::Session(SessionErrorKind::NotFound( + session_id.unwrap_or_else(|| "unknown".into()), + )) + .into()); + } + return Err(Error::from_rpc(err.code, err.message, err.data)); } - return Err(Error::from_rpc(err.code, err.message, err.data)); + Ok(response.result.unwrap_or(serde_json::Value::Null)) } - Ok(response.result.unwrap_or(serde_json::Value::Null)) } /// Send a JSON-RPC response back to the CLI (e.g. for permission or tool call requests). @@ -2534,6 +2615,31 @@ impl Client { self.inner.router.register(session_id) } + /// Like [`register_session`](Self::register_session), but keeps the + /// registration it displaced so a failed resume can restore it with + /// [`restore_session_owned`](Self::restore_session_owned). + pub(crate) fn replace_session_registration( + &self, + session_id: &SessionId, + ) -> ( + crate::router::SessionRegistration, + Option, + ) { + self.inner.router.ensure_started(&self.inner); + self.inner.router.replace(session_id) + } + + /// Unregister the registration identified by `token`, handing the ID back + /// to `replaced` if that session is still running. + pub(crate) fn restore_session_owned( + &self, + session_id: &SessionId, + token: crate::router::RegistrationToken, + replaced: Option, + ) { + self.inner.router.restore_owned(session_id, token, replaced); + } + /// Unregister a session only if `token` still identifies the live /// registration. /// @@ -3245,814 +3351,4 @@ impl Drop for ClientInner { } #[cfg(all(test, feature = "runtime"))] -mod tests { - use super::*; - - #[test] - fn is_transport_failure_matches_request_cancelled() { - let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)); - assert!(err.is_transport_failure()); - } - - #[test] - fn is_transport_failure_matches_io_error() { - let err = Error::from(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "gone")); - assert!(err.is_transport_failure()); - } - - #[test] - fn is_transport_failure_rejects_rpc_error() { - let err = Error::with_message(ErrorKind::Rpc { code: -1 }, "bad"); - assert!(!err.is_transport_failure()); - } - - #[test] - fn is_transport_failure_rejects_session_error() { - let err = Error::from(ErrorKind::Session(SessionErrorKind::NotFound("s1".into()))); - assert!(!err.is_transport_failure()); - } - - #[test] - fn client_options_builder_composes() { - let opts = ClientOptions::new() - .with_program(CliProgram::Path(PathBuf::from("/usr/local/bin/copilot"))) - .with_prefix_args(["node"]) - .with_cwd(PathBuf::from("/tmp")) - .with_env([("KEY", "value")]) - .with_env_remove(["UNWANTED"]) - .with_extra_args(["--quiet"]) - .with_github_token("ghp_test") - .with_use_logged_in_user(false) - .with_log_level(LogLevel::Debug) - .with_session_idle_timeout_seconds(120) - .with_enable_remote_sessions(true); - assert!(matches!(opts.program, CliProgram::Path(_))); - assert_eq!(opts.prefix_args, vec![std::ffi::OsString::from("node")]); - assert_eq!(opts.working_directory, PathBuf::from("/tmp")); - assert_eq!( - opts.env, - vec![( - std::ffi::OsString::from("KEY"), - std::ffi::OsString::from("value") - )] - ); - assert_eq!(opts.env_remove, vec![std::ffi::OsString::from("UNWANTED")]); - assert_eq!(opts.extra_args, vec!["--quiet".to_string()]); - assert_eq!(opts.github_token.as_deref(), Some("ghp_test")); - assert_eq!(opts.use_logged_in_user, Some(false)); - assert!(matches!(opts.log_level, Some(LogLevel::Debug))); - assert_eq!(opts.session_idle_timeout_seconds, Some(120)); - assert!(opts.enable_remote_sessions); - } - - #[test] - fn default_transport_values_resolve_without_process_state() { - assert!(matches!( - resolve_default_transport_value(None).unwrap(), - Transport::Stdio - )); - assert!(matches!( - resolve_default_transport_value(Some("stdio")).unwrap(), - Transport::Stdio - )); - assert!(matches!( - resolve_default_transport_value(Some("INPROCESS")).unwrap(), - Transport::InProcess - )); - assert!(resolve_default_transport_value(Some("tcp")).is_err()); - } - - #[test] - fn inprocess_rejects_process_scoped_options() { - let invalid = [ - ClientOptions::new().with_cwd("."), - ClientOptions::new().with_env([("KEY", "value")]), - ClientOptions::new().with_env_remove(["KEY"]), - ClientOptions::new().with_telemetry(TelemetryConfig::default()), - ClientOptions::new().with_prefix_args(["index.js"]), - ClientOptions::new().with_program(CliProgram::Path("copilot".into())), - ClientOptions::new().with_extra_args(["--verbose"]), - ]; - - for options in invalid { - assert!(validate_inprocess_options(&options).is_err()); - } - } - - #[test] - fn inprocess_allows_typed_runtime_options() { - let options = ClientOptions::new() - .with_base_directory("state") - .with_log_level(LogLevel::Debug) - .with_session_idle_timeout_seconds(10) - .with_github_token("token") - .with_use_logged_in_user(false) - .with_enable_remote_sessions(true); - - assert!(validate_inprocess_options(&options).is_ok()); - } - - #[cfg(not(feature = "in-process"))] - #[tokio::test] - async fn inprocess_requires_cargo_feature() { - let error = Client::start(ClientOptions::new().with_transport(Transport::InProcess)) - .await - .unwrap_err(); - - assert!(error.to_string().contains("in-process")); - } - - #[test] - fn is_transport_failure_rejects_other_protocol_errors() { - let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::CliStartupTimeout)); - assert!(!err.is_transport_failure()); - } - - #[test] - fn build_command_lets_env_remove_strip_injected_token() { - let opts = ClientOptions { - github_token: Some("secret".to_string()), - env_remove: vec![std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN")], - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - // get_envs() iter yields the latest action per key — None means removed. - let action = cmd - .as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) - .map(|(_, v)| v); - assert_eq!( - action, - Some(None), - "env_remove should win over github_token" - ); - } - - #[test] - fn build_command_lets_env_override_injected_token() { - let opts = ClientOptions { - github_token: Some("from-options".to_string()), - env: vec![( - std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN"), - std::ffi::OsString::from("from-env"), - )], - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - let value = cmd - .as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) - .and_then(|(_, v)| v); - assert_eq!(value, Some(std::ffi::OsStr::new("from-env"))); - } - - #[test] - fn build_command_injects_github_token_by_default() { - let opts = ClientOptions { - github_token: Some("just-the-token".to_string()), - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - let value = cmd - .as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) - .and_then(|(_, v)| v); - assert_eq!(value, Some(std::ffi::OsStr::new("just-the-token"))); - } - - fn env_value<'a>(cmd: &'a tokio::process::Command, key: &str) -> Option<&'a std::ffi::OsStr> { - cmd.as_std() - .get_envs() - .find(|(k, _)| *k == std::ffi::OsStr::new(key)) - .and_then(|(_, v)| v) - } - - #[test] - fn telemetry_config_builder_composes() { - let cfg = TelemetryConfig::new() - .with_otlp_endpoint("http://collector:4318") - .with_otlp_protocol(OtlpHttpProtocol::HttpProtobuf) - .with_file_path(PathBuf::from("/var/log/copilot.jsonl")) - .with_exporter_type(OtelExporterType::OtlpHttp) - .with_source_name("my-app") - .with_capture_content(true); - - assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://collector:4318")); - assert_eq!(cfg.otlp_protocol, Some(OtlpHttpProtocol::HttpProtobuf)); - assert_eq!( - cfg.file_path.as_deref(), - Some(Path::new("/var/log/copilot.jsonl")), - ); - assert_eq!(cfg.exporter_type, Some(OtelExporterType::OtlpHttp)); - assert_eq!(cfg.source_name.as_deref(), Some("my-app")); - assert_eq!(cfg.capture_content, Some(true)); - assert!(!cfg.is_empty()); - assert!(TelemetryConfig::new().is_empty()); - } - - #[test] - fn otlp_http_protocol_serde_matches_env_value() { - for (protocol, wire) in [ - (OtlpHttpProtocol::HttpJson, "http/json"), - (OtlpHttpProtocol::HttpProtobuf, "http/protobuf"), - ] { - assert_eq!(protocol.as_str(), wire); - - let serialized = serde_json::to_string(&protocol).unwrap(); - assert_eq!(serialized, format!("\"{wire}\"")); - - let deserialized: OtlpHttpProtocol = serde_json::from_str(&serialized).unwrap(); - assert_eq!(deserialized, protocol); - } - } - - #[test] - fn build_command_sets_otel_env_when_telemetry_enabled() { - let opts = ClientOptions { - telemetry: Some(TelemetryConfig { - otlp_endpoint: Some("http://collector:4318".to_string()), - otlp_protocol: Some(OtlpHttpProtocol::HttpProtobuf), - file_path: Some(PathBuf::from("/var/log/copilot.jsonl")), - exporter_type: Some(OtelExporterType::OtlpHttp), - source_name: Some("my-app".to_string()), - capture_content: Some(true), - }), - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_ENABLED"), - Some(std::ffi::OsStr::new("true")), - ); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), - Some(std::ffi::OsStr::new("http://collector:4318")), - ); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_PROTOCOL"), - Some(std::ffi::OsStr::new("http/protobuf")), - ); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_FILE_EXPORTER_PATH"), - Some(std::ffi::OsStr::new("/var/log/copilot.jsonl")), - ); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_EXPORTER_TYPE"), - Some(std::ffi::OsStr::new("otlp-http")), - ); - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_SOURCE_NAME"), - Some(std::ffi::OsStr::new("my-app")), - ); - assert_eq!( - env_value(&cmd, "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"), - Some(std::ffi::OsStr::new("true")), - ); - } - - #[test] - fn build_command_omits_otel_env_when_telemetry_none() { - let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - for key in [ - "COPILOT_OTEL_ENABLED", - "OTEL_EXPORTER_OTLP_ENDPOINT", - "OTEL_EXPORTER_OTLP_PROTOCOL", - "COPILOT_OTEL_FILE_EXPORTER_PATH", - "COPILOT_OTEL_EXPORTER_TYPE", - "COPILOT_OTEL_SOURCE_NAME", - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", - ] { - assert!( - env_value(&cmd, key).is_none(), - "expected {key} to be unset when telemetry is None", - ); - } - } - - #[test] - fn build_command_omits_unset_telemetry_fields() { - let opts = ClientOptions { - telemetry: Some(TelemetryConfig { - otlp_endpoint: Some("http://collector:4318".to_string()), - ..Default::default() - }), - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - // The one set field plus the implicit enabled flag should propagate. - assert_eq!( - env_value(&cmd, "COPILOT_OTEL_ENABLED"), - Some(std::ffi::OsStr::new("true")), - ); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), - Some(std::ffi::OsStr::new("http://collector:4318")), - ); - // None of the other fields should leak as env vars. - for key in [ - "OTEL_EXPORTER_OTLP_PROTOCOL", - "COPILOT_OTEL_FILE_EXPORTER_PATH", - "COPILOT_OTEL_EXPORTER_TYPE", - "COPILOT_OTEL_SOURCE_NAME", - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", - ] { - assert!(env_value(&cmd, key).is_none(), "{key} should be unset"); - } - } - - #[test] - fn build_command_lets_user_env_override_telemetry() { - let opts = ClientOptions { - telemetry: Some(TelemetryConfig { - otlp_endpoint: Some("http://from-config:4318".to_string()), - ..Default::default() - }), - env: vec![( - std::ffi::OsString::from("OTEL_EXPORTER_OTLP_ENDPOINT"), - std::ffi::OsString::from("http://from-user-env:4318"), - )], - ..Default::default() - }; - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), - Some(std::ffi::OsStr::new("http://from-user-env:4318")), - "user-supplied options.env should override telemetry config", - ); - } - - #[test] - fn build_command_sets_copilot_home_env_when_configured() { - let opts = ClientOptions::new().with_base_directory(PathBuf::from("/custom/copilot")); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "COPILOT_HOME"), - Some(std::ffi::OsStr::new("/custom/copilot")), - ); - - let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert!(env_value(&cmd, "COPILOT_HOME").is_none()); - } - - #[test] - fn build_command_sets_connection_token_env_when_configured() { - let opts = ClientOptions::new().with_transport(Transport::Tcp { - port: 0, - connection_token: Some("secret-token".to_string()), - }); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert_eq!( - env_value(&cmd, "COPILOT_CONNECTION_TOKEN"), - Some(std::ffi::OsStr::new("secret-token")), - ); - - let opts = ClientOptions::default(); - let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); - assert!(env_value(&cmd, "COPILOT_CONNECTION_TOKEN").is_none()); - } - - #[tokio::test] - async fn start_rejects_empty_connection_token() { - let opts = ClientOptions::new() - .with_transport(Transport::Tcp { - port: 0, - connection_token: Some(String::new()), - }) - .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); - let err = Client::start(opts).await.unwrap_err(); - assert!( - matches!(err.kind(), ErrorKind::InvalidConfig), - "got {err:?}" - ); - } - - #[tokio::test] - async fn start_rejects_empty_external_connection_token() { - let opts = ClientOptions::new() - .with_transport(Transport::External { - host: "127.0.0.1".to_string(), - port: 1, - connection_token: Some(String::new()), - }) - .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); - let err = Client::start(opts).await.unwrap_err(); - assert!( - matches!(err.kind(), ErrorKind::InvalidConfig), - "got {err:?}" - ); - } - - #[test] - fn telemetry_config_capture_content_serializes_as_lowercase_bool() { - let opts_true = ClientOptions { - telemetry: Some(TelemetryConfig { - capture_content: Some(true), - ..Default::default() - }), - ..Default::default() - }; - let opts_false = ClientOptions { - telemetry: Some(TelemetryConfig { - capture_content: Some(false), - ..Default::default() - }), - ..Default::default() - }; - let cmd_true = Client::build_command(Path::new("/bin/echo"), &opts_true, Path::new("/tmp")); - let cmd_false = - Client::build_command(Path::new("/bin/echo"), &opts_false, Path::new("/tmp")); - assert_eq!( - env_value( - &cmd_true, - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" - ), - Some(std::ffi::OsStr::new("true")), - ); - assert_eq!( - env_value( - &cmd_false, - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" - ), - Some(std::ffi::OsStr::new("false")), - ); - } - - #[test] - fn session_idle_timeout_args_are_omitted_by_default() { - let opts = ClientOptions::default(); - assert!(Client::session_idle_timeout_args(&opts).is_empty()); - } - - #[test] - fn session_idle_timeout_args_omitted_for_zero() { - let opts = ClientOptions { - session_idle_timeout_seconds: Some(0), - ..Default::default() - }; - assert!(Client::session_idle_timeout_args(&opts).is_empty()); - } - - #[test] - fn session_idle_timeout_args_emit_flag_for_positive_value() { - let opts = ClientOptions { - session_idle_timeout_seconds: Some(300), - ..Default::default() - }; - assert_eq!( - Client::session_idle_timeout_args(&opts), - vec!["--session-idle-timeout".to_string(), "300".to_string()] - ); - } - - #[test] - fn remote_args_omitted_by_default() { - let opts = ClientOptions::default(); - assert!(Client::remote_args(&opts).is_empty()); - } - - #[test] - fn remote_args_emit_flag_when_enabled() { - let opts = ClientOptions { - enable_remote_sessions: true, - ..Default::default() - }; - assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); - } - - #[test] - fn log_level_args_omitted_when_unset() { - let opts = ClientOptions::default(); - assert!(opts.log_level.is_none()); - assert!( - Client::log_level_args(&opts).is_empty(), - "with no caller-supplied log_level the SDK must not pass --log-level" - ); - } - - #[test] - fn log_level_args_emit_flag_when_set() { - let opts = ClientOptions::default().with_log_level(LogLevel::Debug); - assert_eq!(Client::log_level_args(&opts), vec!["--log-level", "debug"]); - } - - #[test] - fn cli_mode_opts_into_process_logging_without_changing_empty_mode_environment() { - for (mode, expected) in [ - (ClientMode::Empty, None), - (ClientMode::CopilotCli, Some("1")), - ] { - let mut options = ClientOptions::default().with_mode(mode); - options.env.push(( - std::ffi::OsString::from("COPILOT_RUNTIME_PROCESS_FILE_LOGGING"), - std::ffi::OsString::from("opposite"), - )); - let command = - Client::build_command(Path::new("copilot-runtime"), &options, Path::new(".")); - let actual = command - .as_std() - .get_envs() - .find(|(key, _)| { - *key == std::ffi::OsStr::new("COPILOT_RUNTIME_PROCESS_FILE_LOGGING") - }) - .and_then(|(_, value)| value); - assert_eq!( - actual, - Some(std::ffi::OsStr::new(expected.unwrap_or("opposite"))), - "mode: {mode:?}" - ); - } - } - - #[test] - fn log_level_str_round_trips() { - for level in [ - LogLevel::None, - LogLevel::Error, - LogLevel::Warning, - LogLevel::Info, - LogLevel::Debug, - LogLevel::All, - ] { - let s = level.as_str(); - let json = serde_json::to_string(&level).unwrap(); - assert_eq!(json, format!("\"{s}\"")); - let parsed: LogLevel = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed, level); - } - } - - #[test] - fn client_options_debug_redacts_handler() { - struct StubHandler; - #[async_trait] - impl ListModelsHandler for StubHandler { - async fn list_models(&self) -> Result> { - Ok(vec![]) - } - } - let opts = ClientOptions { - on_list_models: Some(Arc::new(StubHandler)), - github_token: Some("secret-token".into()), - ..Default::default() - }; - let debug = format!("{opts:?}"); - assert!(debug.contains("on_list_models: Some(\"\")")); - assert!(debug.contains("github_token: Some(\"\")")); - assert!(!debug.contains("secret-token")); - } - - #[tokio::test] - async fn list_models_uses_on_list_models_handler_when_set() { - use std::sync::atomic::{AtomicUsize, Ordering}; - - struct CountingHandler { - calls: Arc, - models: Vec, - } - #[async_trait] - impl ListModelsHandler for CountingHandler { - async fn list_models(&self) -> Result> { - self.calls.fetch_add(1, Ordering::SeqCst); - Ok(self.models.clone()) - } - } - - let calls = Arc::new(AtomicUsize::new(0)); - let model = Model { - id: "byok-gpt-4".into(), - name: "BYOK GPT-4".into(), - ..Default::default() - }; - let handler: Arc = Arc::new(CountingHandler { - calls: Arc::clone(&calls), - models: vec![model.clone()], - }); - - let client = client_with_list_models_handler(handler); - - let result = client.list_models().await.unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].id, "byok-gpt-4"); - assert_eq!(calls.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn list_models_serializes_concurrent_cache_misses() { - use std::sync::atomic::{AtomicUsize, Ordering}; - - struct SlowCountingHandler { - calls: Arc, - models: Vec, - } - #[async_trait] - impl ListModelsHandler for SlowCountingHandler { - async fn list_models(&self) -> Result> { - self.calls.fetch_add(1, Ordering::SeqCst); - tokio::time::sleep(std::time::Duration::from_millis(25)).await; - Ok(self.models.clone()) - } - } - - let calls = Arc::new(AtomicUsize::new(0)); - let model = Model { - id: "single-flight-model".into(), - name: "Single Flight Model".into(), - ..Default::default() - }; - let handler: Arc = Arc::new(SlowCountingHandler { - calls: Arc::clone(&calls), - models: vec![model], - }); - let client = client_with_list_models_handler(handler); - - let (first, second) = tokio::join!(client.list_models(), client.list_models()); - assert_eq!(first.unwrap()[0].id, "single-flight-model"); - assert_eq!(second.unwrap()[0].id, "single-flight-model"); - assert_eq!(calls.load(Ordering::SeqCst), 1); - } - - #[tokio::test] - async fn cancelled_resume_session_unregisters_pending_session() { - let (client_write, _server_read) = tokio::io::duplex(8192); - let (_server_write, client_read) = tokio::io::duplex(8192); - let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); - assert!(client.startup_timings().is_none()); - let session_id = SessionId::new("resume-cancel-test"); - let handle = tokio::spawn({ - let client = client.clone(); - async move { - client - .resume_session(ResumeSessionConfig::new(session_id)) - .await - } - }); - - wait_for_pending_session_registration(&client).await; - handle.abort(); - let _ = handle.await; - - assert!(client.inner.router.session_ids().is_empty()); - client.force_stop(); - } - - #[cfg(any(unix, windows))] - #[tokio::test] - async fn dropping_last_client_kills_spawned_cli() { - let temp = tempfile::tempdir().unwrap(); - let ready = temp.path().join("ready"); - let survived = temp.path().join("survived"); - let child = test_child_command(temp.path(), &ready, &survived) - .spawn() - .unwrap(); - let (client_write, _server_read) = tokio::io::duplex(64); - let (_server_write, client_read) = tokio::io::duplex(64); - let client = Client::from_transport( - client_read, - client_write, - Some(child), - None, - temp.path().to_path_buf(), - None, - None, - false, - false, - None, - None, - None, - ClientMode::default(), - None, - false, - ) - .unwrap(); - - wait_for_test_child(&ready).await; - drop(client); - - assert_test_child_killed(&survived).await; - } - - #[cfg(any(unix, windows))] - #[tokio::test] - async fn spawned_child_is_killed_when_dropped() { - let temp = tempfile::tempdir().unwrap(); - let ready = temp.path().join("ready"); - let survived = temp.path().join("survived"); - let child = test_child_command(temp.path(), &ready, &survived) - .spawn() - .unwrap(); - - wait_for_test_child(&ready).await; - drop(child); - - assert_test_child_killed(&survived).await; - } - - #[cfg(any(unix, windows))] - fn test_child_command(temp: &Path, ready: &Path, survived: &Path) -> Command { - let mut command = Client::build_command(Path::new("node"), &ClientOptions::default(), temp); - #[cfg(windows)] - { - const CREATE_NO_WINDOW: u32 = 0x0800_0000; - command.creation_flags(CREATE_NO_WINDOW); - } - command - .args([ - "-e", - r#" - const fs = require("node:fs"); - fs.writeFileSync(process.env.READY, "ready"); - setTimeout(() => fs.writeFileSync(process.env.SURVIVED, "survived"), 1000); - "#, - ]) - .env("READY", ready) - .env("SURVIVED", survived) - .stderr(Stdio::inherit()); - command - } - - #[cfg(any(unix, windows))] - async fn wait_for_test_child(ready: &Path) { - let deadline = tokio::time::Instant::now() + Duration::from_secs(30); - while !ready.exists() { - assert!( - tokio::time::Instant::now() < deadline, - "child did not report readiness" - ); - tokio::time::sleep(Duration::from_millis(10)).await; - } - } - - #[cfg(any(unix, windows))] - async fn assert_test_child_killed(survived: &Path) { - tokio::time::sleep(Duration::from_millis(1500)).await; - - assert!( - !survived.exists(), - "child survived after its owner was dropped" - ); - } - - fn client_with_list_models_handler(handler: Arc) -> Client { - Client { - ahp_host_sessions: None, - inner: Arc::new(ClientInner { - ahp_host_callbacks: Arc::new(ahp_host::ExitCallbacks::default()), - ahp_host_sessions: std::sync::Weak::new(), - child: parking_lot::Mutex::new(None), - owns_stdio: false, - force_stop_requested: tokio_util::sync::CancellationToken::new(), - process_tree: parking_lot::Mutex::new(None), - #[cfg(feature = "in-process")] - ffi_host: parking_lot::Mutex::new(None), - rpc: { - let (req_tx, _req_rx) = mpsc::unbounded_channel(); - let (notif_tx, _notif_rx) = broadcast::channel(16); - let (read_pipe, _write_pipe) = tokio::io::duplex(64); - let (_unused_read, write_pipe) = tokio::io::duplex(64); - JsonRpcClient::new(write_pipe, read_pipe, notif_tx, req_tx) - }, - cwd: PathBuf::from("."), - request_rx: parking_lot::Mutex::new(None), - notification_tx: broadcast::channel(16).0, - router: router::SessionRouter::new(), - github_token_registry: Arc::new(github_token::GitHubTokenRegistry::new()), - negotiated_protocol_version: OnceLock::new(), - state: parking_lot::Mutex::new(ConnectionState::Connected), - lifecycle_tx: broadcast::channel(16).0, - on_list_models: Some(handler), - models_cache: parking_lot::Mutex::new(Arc::new(tokio::sync::OnceCell::new())), - session_fs_configured: false, - session_fs_sqlite_declared: false, - llm_inference: OnceLock::new(), - extension_launch_provider: Arc::new( - extension_launch_provider::ExtensionLaunchProviderDispatcher::new(None), - ), - installation_confirmation: Arc::new( - installation_confirmation::InstallationConfirmationDispatcher::new(), - ), - on_github_telemetry: None, - on_get_trace_context: None, - effective_connection_token: None, - mode: ClientMode::default(), - client_info: None, - startup_timings: OnceLock::new(), - }), - } - } - - async fn wait_for_pending_session_registration(client: &Client) { - let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1); - while client.inner.router.session_ids().is_empty() { - assert!( - tokio::time::Instant::now() < deadline, - "session was not registered" - ); - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - } -} +mod tests; diff --git a/rust/src/mode.rs b/rust/src/mode.rs index 2b1ab897ce..671b32ec7d 100644 --- a/rust/src/mode.rs +++ b/rust/src/mode.rs @@ -301,270 +301,4 @@ pub(crate) fn experimental_mode_for_mode(mode: ClientMode, supplied: Option = ToolSet::new().add_mcp("*").unwrap().into(); - assert_eq!(v, vec!["mcp:*"]); - } - - #[test] - fn validate_tool_filter_list_rejects_bare_star() { - let bad = vec!["*".to_string()]; - assert!(validate_tool_filter_list("availableTools", Some(&bad)).is_err()); - } - - #[test] - fn validate_tool_filter_list_allows_qualified_star() { - let ok = vec!["builtin:*".to_string(), "mcp:*".to_string()]; - assert!(validate_tool_filter_list("availableTools", Some(&ok)).is_ok()); - } - - #[test] - fn validate_tool_filter_list_none_is_ok() { - assert!(validate_tool_filter_list("availableTools", None).is_ok()); - } - - #[test] - fn builtin_tools_isolated_contents() { - assert!(BUILTIN_TOOLS_ISOLATED.contains(&"ask_user")); - assert!(BUILTIN_TOOLS_ISOLATED.contains(&"task_complete")); - assert!(BUILTIN_TOOLS_ISOLATED.contains(&"skill")); - assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"bash")); - assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"edit")); - assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"web_fetch")); - } - - #[test] - fn client_mode_default_is_copilot_cli() { - assert_eq!(ClientMode::default(), ClientMode::CopilotCli); - } - - #[test] - fn system_message_copilot_cli_passes_through_unchanged() { - let cfg = SystemMessageConfig { - mode: Some("append".to_string()), - content: Some("hello".to_string()), - sections: None, - }; - let out = system_message_for_mode(ClientMode::CopilotCli, Some(cfg.clone())); - let out = out.unwrap(); - assert_eq!(out.mode.as_deref(), Some("append")); - assert_eq!(out.content.as_deref(), Some("hello")); - } - - #[test] - fn system_message_empty_none_injects_strip() { - let out = system_message_for_mode(ClientMode::Empty, None).unwrap(); - assert_eq!(out.mode.as_deref(), Some("customize")); - let sections = out.sections.unwrap(); - let env = sections.get("environment_context").unwrap(); - assert_eq!(env.action.as_deref(), Some("remove")); - } - - #[test] - fn system_message_empty_append_promoted_to_customize() { - let cfg = SystemMessageConfig { - mode: Some("append".to_string()), - content: Some("hi".to_string()), - sections: None, - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); - assert_eq!(out.mode.as_deref(), Some("customize")); - assert_eq!(out.content.as_deref(), Some("hi")); - let sections = out.sections.unwrap(); - assert!(sections.contains_key("environment_context")); - } - - #[test] - fn system_message_empty_replace_passes_through() { - let cfg = SystemMessageConfig { - mode: Some("replace".to_string()), - content: Some("verbatim".to_string()), - sections: None, - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg.clone())).unwrap(); - assert_eq!(out.mode.as_deref(), Some("replace")); - assert_eq!(out.content.as_deref(), Some("verbatim")); - assert!(out.sections.is_none()); - } - - #[test] - fn system_message_empty_customize_with_env_context_preserved() { - let mut sections = HashMap::new(); - sections.insert( - "environment_context".to_string(), - SectionOverride { - action: Some("replace".to_string()), - content: Some("custom env".to_string()), - }, - ); - let cfg = SystemMessageConfig { - mode: Some("customize".to_string()), - content: None, - sections: Some(sections), - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); - let env = out.sections.unwrap().remove("environment_context").unwrap(); - assert_eq!(env.action.as_deref(), Some("replace")); - assert_eq!(env.content.as_deref(), Some("custom env")); - } - - #[test] - fn system_message_empty_customize_without_env_context_gets_strip() { - let mut sections = HashMap::new(); - sections.insert( - "other_section".to_string(), - SectionOverride { - action: Some("replace".to_string()), - content: Some("body".to_string()), - }, - ); - let cfg = SystemMessageConfig { - mode: Some("customize".to_string()), - content: None, - sections: Some(sections), - }; - let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); - let secs = out.sections.unwrap(); - assert!(secs.contains_key("other_section")); - let env = secs.get("environment_context").unwrap(); - assert_eq!(env.action.as_deref(), Some("remove")); - } - - #[test] - fn memory_copilot_cli_leaves_unset_when_not_supplied() { - assert_eq!(memory_for_mode(ClientMode::CopilotCli, None), None); - } - - #[test] - fn memory_copilot_cli_preserves_supplied() { - assert_eq!( - memory_for_mode(ClientMode::CopilotCli, Some(MemoryConfiguration::enabled())), - Some(MemoryConfiguration::enabled()) - ); - } - - #[test] - fn memory_empty_defaults_to_disabled() { - assert_eq!( - memory_for_mode(ClientMode::Empty, None), - Some(MemoryConfiguration::disabled()) - ); - } - - #[test] - fn memory_empty_preserves_supplied() { - assert_eq!( - memory_for_mode(ClientMode::Empty, Some(MemoryConfiguration::enabled())), - Some(MemoryConfiguration::enabled()) - ); - } - - #[test] - fn experimental_mode_defaults_false_in_empty_mode() { - assert_eq!( - experimental_mode_for_mode(ClientMode::Empty, None), - Some(false) - ); - assert_eq!( - experimental_mode_for_mode(ClientMode::Empty, Some(true)), - Some(true) - ); - assert_eq!( - experimental_mode_for_mode(ClientMode::Empty, Some(false)), - Some(false) - ); - } - - #[test] - fn experimental_mode_remains_runtime_controlled_in_copilot_cli_mode() { - assert_eq!( - experimental_mode_for_mode(ClientMode::CopilotCli, None), - None - ); - } -} +mod tests; diff --git a/rust/src/mode/tests.rs b/rust/src/mode/tests.rs new file mode 100644 index 0000000000..4c486fd7bf --- /dev/null +++ b/rust/src/mode/tests.rs @@ -0,0 +1,271 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn custom_agents_local_only_respects_mode_and_caller_value() { + assert_eq!( + resolve_custom_agents_local_only(ClientMode::Empty, None), + Some(true) + ); + assert_eq!( + resolve_custom_agents_local_only(ClientMode::Empty, Some(false)), + Some(false) + ); + assert_eq!( + resolve_custom_agents_local_only(ClientMode::CopilotCli, None), + None + ); +} + +#[test] +fn tool_set_emits_source_qualified_patterns() { + let v = ToolSet::new() + .add_builtin("bash") + .unwrap() + .add_builtin("*") + .unwrap() + .add_custom("foo") + .unwrap() + .add_custom("*") + .unwrap() + .add_mcp("github-list_issues") + .unwrap() + .add_mcp("*") + .unwrap() + .to_vec(); + assert_eq!( + v, + vec![ + "builtin:bash", + "builtin:*", + "custom:foo", + "custom:*", + "mcp:github-list_issues", + "mcp:*", + ] + ); +} + +#[test] +fn tool_set_add_builtin_many() { + let v = ToolSet::new() + .add_builtin_many(BUILTIN_TOOLS_ISOLATED) + .unwrap() + .into_vec(); + assert_eq!(v.len(), BUILTIN_TOOLS_ISOLATED.len()); + assert_eq!(v[0], format!("builtin:{}", BUILTIN_TOOLS_ISOLATED[0])); +} + +#[test] +fn tool_set_rejects_invalid_names() { + for bad in ["bash!", "with space", "colon:name", "", "wild*card"] { + assert!( + ToolSet::new().add_builtin(bad).is_err(), + "expected '{bad}' to be rejected" + ); + assert!(ToolSet::new().add_custom(bad).is_err()); + assert!(ToolSet::new().add_mcp(bad).is_err()); + } +} + +#[test] +fn tool_set_accepts_wildcard_and_underscores_and_dashes() { + assert!(ToolSet::new().add_builtin("*").is_ok()); + assert!(ToolSet::new().add_mcp("github-list_issues").is_ok()); + assert!(ToolSet::new().add_custom("A_b-9").is_ok()); +} + +#[test] +fn into_vec_is_idempotent_with_to_vec() { + let ts = ToolSet::new().add_builtin("bash").unwrap(); + assert_eq!(ts.to_vec(), vec!["builtin:bash"]); + assert_eq!(ts.into_vec(), vec!["builtin:bash"]); +} + +#[test] +fn into_vec_string_conversion() { + let v: Vec = ToolSet::new().add_mcp("*").unwrap().into(); + assert_eq!(v, vec!["mcp:*"]); +} + +#[test] +fn validate_tool_filter_list_rejects_bare_star() { + let bad = vec!["*".to_string()]; + assert!(validate_tool_filter_list("availableTools", Some(&bad)).is_err()); +} + +#[test] +fn validate_tool_filter_list_allows_qualified_star() { + let ok = vec!["builtin:*".to_string(), "mcp:*".to_string()]; + assert!(validate_tool_filter_list("availableTools", Some(&ok)).is_ok()); +} + +#[test] +fn validate_tool_filter_list_none_is_ok() { + assert!(validate_tool_filter_list("availableTools", None).is_ok()); +} + +#[test] +fn builtin_tools_isolated_contents() { + assert!(BUILTIN_TOOLS_ISOLATED.contains(&"ask_user")); + assert!(BUILTIN_TOOLS_ISOLATED.contains(&"task_complete")); + assert!(BUILTIN_TOOLS_ISOLATED.contains(&"skill")); + assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"bash")); + assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"edit")); + assert!(!BUILTIN_TOOLS_ISOLATED.contains(&"web_fetch")); +} + +#[test] +fn client_mode_default_is_copilot_cli() { + assert_eq!(ClientMode::default(), ClientMode::CopilotCli); +} + +#[test] +fn system_message_copilot_cli_passes_through_unchanged() { + let cfg = SystemMessageConfig { + mode: Some("append".to_string()), + content: Some("hello".to_string()), + sections: None, + }; + let out = system_message_for_mode(ClientMode::CopilotCli, Some(cfg.clone())); + let out = out.unwrap(); + assert_eq!(out.mode.as_deref(), Some("append")); + assert_eq!(out.content.as_deref(), Some("hello")); +} + +#[test] +fn system_message_empty_none_injects_strip() { + let out = system_message_for_mode(ClientMode::Empty, None).unwrap(); + assert_eq!(out.mode.as_deref(), Some("customize")); + let sections = out.sections.unwrap(); + let env = sections.get("environment_context").unwrap(); + assert_eq!(env.action.as_deref(), Some("remove")); +} + +#[test] +fn system_message_empty_append_promoted_to_customize() { + let cfg = SystemMessageConfig { + mode: Some("append".to_string()), + content: Some("hi".to_string()), + sections: None, + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); + assert_eq!(out.mode.as_deref(), Some("customize")); + assert_eq!(out.content.as_deref(), Some("hi")); + let sections = out.sections.unwrap(); + assert!(sections.contains_key("environment_context")); +} + +#[test] +fn system_message_empty_replace_passes_through() { + let cfg = SystemMessageConfig { + mode: Some("replace".to_string()), + content: Some("verbatim".to_string()), + sections: None, + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg.clone())).unwrap(); + assert_eq!(out.mode.as_deref(), Some("replace")); + assert_eq!(out.content.as_deref(), Some("verbatim")); + assert!(out.sections.is_none()); +} + +#[test] +fn system_message_empty_customize_with_env_context_preserved() { + let mut sections = HashMap::new(); + sections.insert( + "environment_context".to_string(), + SectionOverride { + action: Some("replace".to_string()), + content: Some("custom env".to_string()), + }, + ); + let cfg = SystemMessageConfig { + mode: Some("customize".to_string()), + content: None, + sections: Some(sections), + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); + let env = out.sections.unwrap().remove("environment_context").unwrap(); + assert_eq!(env.action.as_deref(), Some("replace")); + assert_eq!(env.content.as_deref(), Some("custom env")); +} + +#[test] +fn system_message_empty_customize_without_env_context_gets_strip() { + let mut sections = HashMap::new(); + sections.insert( + "other_section".to_string(), + SectionOverride { + action: Some("replace".to_string()), + content: Some("body".to_string()), + }, + ); + let cfg = SystemMessageConfig { + mode: Some("customize".to_string()), + content: None, + sections: Some(sections), + }; + let out = system_message_for_mode(ClientMode::Empty, Some(cfg)).unwrap(); + let secs = out.sections.unwrap(); + assert!(secs.contains_key("other_section")); + let env = secs.get("environment_context").unwrap(); + assert_eq!(env.action.as_deref(), Some("remove")); +} + +#[test] +fn memory_copilot_cli_leaves_unset_when_not_supplied() { + assert_eq!(memory_for_mode(ClientMode::CopilotCli, None), None); +} + +#[test] +fn memory_copilot_cli_preserves_supplied() { + assert_eq!( + memory_for_mode(ClientMode::CopilotCli, Some(MemoryConfiguration::enabled())), + Some(MemoryConfiguration::enabled()) + ); +} + +#[test] +fn memory_empty_defaults_to_disabled() { + assert_eq!( + memory_for_mode(ClientMode::Empty, None), + Some(MemoryConfiguration::disabled()) + ); +} + +#[test] +fn memory_empty_preserves_supplied() { + assert_eq!( + memory_for_mode(ClientMode::Empty, Some(MemoryConfiguration::enabled())), + Some(MemoryConfiguration::enabled()) + ); +} + +#[test] +fn experimental_mode_defaults_false_in_empty_mode() { + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, None), + Some(false) + ); + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, Some(true)), + Some(true) + ); + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, Some(false)), + Some(false) + ); +} + +#[test] +fn experimental_mode_remains_runtime_controlled_in_copilot_cli_mode() { + assert_eq!( + experimental_mode_for_mode(ClientMode::CopilotCli, None), + None + ); +} diff --git a/rust/src/permission.rs b/rust/src/permission.rs index 57c0785708..6fe4f6bc94 100644 --- a/rust/src/permission.rs +++ b/rust/src/permission.rs @@ -135,153 +135,4 @@ impl PermissionHandler for PolicyHandler { } #[cfg(test)] -mod tests { - use super::*; - - fn data() -> PermissionRequestData { - PermissionRequestData { - extra: serde_json::json!({ "tool": "shell" }), - ..Default::default() - } - } - - #[tokio::test] - async fn approve_all_approves() { - let h = approve_all(); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_fails_when_managed_settings_enabled() { - let h = approve_all(); - let mut request = data(); - request.managed_settings_enabled = true; - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), request) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::UserNotAvailable(_), - .. - } - )); - } - - #[tokio::test] - async fn deny_all_denies() { - let h = deny_all(); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_if_consults_predicate() { - let h = approve_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_if_leaves_managed_approval_pending_when_predicate_approves() { - let h = approve_if(|_| true); - let mut request = data(); - request.managed_approval_required = Some(true); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), request) - .await, - PermissionResult::NoResult - )); - } - - #[tokio::test] - async fn approve_if_still_rejects_managed_request_when_predicate_denies() { - let h = approve_if(|_| false); - let mut request = data(); - request.managed_approval_required = Some(true); - assert!(matches!( - h.handle(SessionId::from("s"), RequestId::new("1"), request) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn resolve_handler_policy_wins() { - struct AlwaysApprove; - #[async_trait] - impl PermissionHandler for AlwaysApprove { - async fn handle( - &self, - _: SessionId, - _: RequestId, - _: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::approve_once() - } - } - let resolved = - resolve_handler(Some(Arc::new(AlwaysApprove)), Some(Policy::DenyAll)).unwrap(); - // Policy wins -- the AlwaysApprove handler is discarded. - assert!(matches!( - resolved - .handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn resolve_handler_with_only_handler() { - struct H; - #[async_trait] - impl PermissionHandler for H { - async fn handle( - &self, - _: SessionId, - _: RequestId, - _: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::approve_once() - } - } - let resolved = resolve_handler(Some(Arc::new(H)), None).unwrap(); - assert!(matches!( - resolved - .handle(SessionId::from("s"), RequestId::new("1"), data()) - .await, - PermissionResult::Decision { - decision: crate::types::PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[test] - fn resolve_handler_with_neither_returns_none() { - assert!(resolve_handler(None, None).is_none()); - } -} +mod tests; diff --git a/rust/src/permission/tests.rs b/rust/src/permission/tests.rs new file mode 100644 index 0000000000..0a4afab545 --- /dev/null +++ b/rust/src/permission/tests.rs @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +fn data() -> PermissionRequestData { + PermissionRequestData { + extra: serde_json::json!({ "tool": "shell" }), + ..Default::default() + } +} + +#[tokio::test] +async fn approve_all_approves() { + let h = approve_all(); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_fails_when_managed_settings_enabled() { + let h = approve_all(); + let mut request = data(); + request.managed_settings_enabled = true; + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), request) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::UserNotAvailable(_), + .. + } + )); +} + +#[tokio::test] +async fn deny_all_denies() { + let h = deny_all(); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_if_consults_predicate() { + let h = approve_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_if_leaves_managed_approval_pending_when_predicate_approves() { + let h = approve_if(|_| true); + let mut request = data(); + request.managed_approval_required = Some(true); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), request) + .await, + PermissionResult::NoResult + )); +} + +#[tokio::test] +async fn approve_if_still_rejects_managed_request_when_predicate_denies() { + let h = approve_if(|_| false); + let mut request = data(); + request.managed_approval_required = Some(true); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), request) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn resolve_handler_policy_wins() { + struct AlwaysApprove; + #[async_trait] + impl PermissionHandler for AlwaysApprove { + async fn handle( + &self, + _: SessionId, + _: RequestId, + _: PermissionRequestData, + ) -> PermissionResult { + PermissionResult::approve_once() + } + } + let resolved = resolve_handler(Some(Arc::new(AlwaysApprove)), Some(Policy::DenyAll)).unwrap(); + // Policy wins -- the AlwaysApprove handler is discarded. + assert!(matches!( + resolved + .handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn resolve_handler_with_only_handler() { + struct H; + #[async_trait] + impl PermissionHandler for H { + async fn handle( + &self, + _: SessionId, + _: RequestId, + _: PermissionRequestData, + ) -> PermissionResult { + PermissionResult::approve_once() + } + } + let resolved = resolve_handler(Some(Arc::new(H)), None).unwrap(); + assert!(matches!( + resolved + .handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision { + decision: crate::types::PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[test] +fn resolve_handler_with_neither_returns_none() { + assert!(resolve_handler(None, None).is_none()); +} diff --git a/rust/src/resolve.rs b/rust/src/resolve.rs index 1fbeb7c9f7..7809d993d8 100644 --- a/rust/src/resolve.rs +++ b/rust/src/resolve.rs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + //! Internal resolution of the GitHub Copilot CLI binary. //! //! Resolution order: @@ -101,7 +103,7 @@ pub(crate) fn copilot_binary_with_extract_dir( /// `COPILOT_SKIP_CLI_DOWNLOAD`). /// /// The path is recomputed from the build-time-baked -/// `COPILOT_SDK_CLI_VERSION`, the OS-derived binary name, and the +/// `COPILOT_SDK_CLI_CACHE_ID`, the OS-derived binary name, and the /// optional `COPILOT_CLI_EXTRACT_DIR` env var. This must match /// the build script exactly; both use `cache_paths` so the convention /// cannot drift. We deliberately don't bake the resolved path into the @@ -110,8 +112,8 @@ pub(crate) fn copilot_binary_with_extract_dir( /// and prevents copying `target/` between hosts. #[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))] fn extracted_program(use_runtime_wrapper: bool) -> Option { - let version = env!("COPILOT_SDK_CLI_VERSION"); - let dir = crate::cache_paths::extracted_runtime_install_dir(version); + let cache_identity = env!("COPILOT_SDK_CLI_CACHE_ID"); + let dir = crate::cache_paths::extracted_runtime_install_dir(cache_identity); let path = dir.join(if use_runtime_wrapper { runtime_binary_name() @@ -215,27 +217,4 @@ fn runtime_binary_name() -> &'static str { } #[cfg(test)] -mod tests { - use std::fs; - - use tempfile::tempdir; - - use super::validate_runtime_pair; - - #[test] - fn runtime_pair_requires_adjacent_nonempty_runtime_node() { - let dir = tempdir().expect("temp dir"); - let wrapper = dir.path().join(if cfg!(windows) { - "copilot-runtime.exe" - } else { - "copilot-runtime" - }); - fs::write(&wrapper, b"wrapper").expect("write wrapper"); - - let error = validate_runtime_pair(&wrapper).expect_err("runtime.node is required"); - assert!(error.to_string().contains("runtime.node")); - - fs::write(dir.path().join("runtime.node"), b"runtime").expect("write runtime.node"); - validate_runtime_pair(&wrapper).expect("complete pair is valid"); - } -} +mod tests; diff --git a/rust/src/resolve/tests.rs b/rust/src/resolve/tests.rs new file mode 100644 index 0000000000..526cc08bb6 --- /dev/null +++ b/rust/src/resolve/tests.rs @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::fs; + +use tempfile::tempdir; + +use super::validate_runtime_pair; + +#[test] +fn runtime_pair_requires_adjacent_nonempty_runtime_node() { + let dir = tempdir().expect("temp dir"); + let wrapper = dir.path().join(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }); + fs::write(&wrapper, b"wrapper").expect("write wrapper"); + + let error = validate_runtime_pair(&wrapper).expect_err("runtime.node is required"); + assert!(error.to_string().contains("runtime.node")); + + fs::write(dir.path().join("runtime.node"), b"runtime").expect("write runtime.node"); + validate_runtime_pair(&wrapper).expect("complete pair is valid"); +} diff --git a/rust/src/router.rs b/rust/src/router.rs index 89d7a988fc..9b0d68464b 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Weak}; use parking_lot::Mutex; use tokio::sync::{broadcast, mpsc}; use tracing::warn; -use crate::jsonrpc::JsonRpcRequest; +use crate::jsonrpc::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, error_codes}; use crate::types::{SessionEventNotification, SessionId}; /// Identity of one specific registration of a session ID. @@ -41,6 +41,13 @@ struct SessionSenders { token: RegistrationToken, } +/// A registration displaced by [`SessionRouter::replace`]. +/// +/// Holding it keeps the displaced session's channels open, so a failed +/// resume can hand routing back with [`SessionRouter::restore_owned`]. +/// Dropping it closes them, which ends that session's event loop. +pub(crate) struct ReplacedRegistration(SessionSenders); + /// Routes notifications and requests by sessionId to per-session channels. /// /// Internal to the SDK — consumers interact via `Client::register_session()`. @@ -64,10 +71,20 @@ impl SessionRouter { /// Replaces any existing registration for the same ID and returns a /// fresh [`RegistrationToken`] identifying this registration. pub(crate) fn register(&self, session_id: &SessionId) -> SessionRegistration { + self.replace(session_id).0 + } + + /// Like [`register`](Self::register), but also returns the registration + /// it displaced so the caller can [`restore_owned`](Self::restore_owned) + /// it if the new one is abandoned. + pub(crate) fn replace( + &self, + session_id: &SessionId, + ) -> (SessionRegistration, Option) { let (notif_tx, notif_rx) = mpsc::unbounded_channel(); let (req_tx, req_rx) = mpsc::unbounded_channel(); let token = RegistrationToken(self.next_token.fetch_add(1, Ordering::Relaxed)); - self.sessions.lock().insert( + let replaced = self.sessions.lock().insert( session_id.clone(), SessionSenders { notifications: notif_tx, @@ -75,13 +92,14 @@ impl SessionRouter { token, }, ); - SessionRegistration { + let registration = SessionRegistration { channels: SessionChannels { notifications: notif_rx, requests: req_rx, }, token, - } + }; + (registration, replaced.map(ReplacedRegistration)) } /// Unregister a session, dropping its channels. @@ -115,17 +133,37 @@ impl SessionRouter { &self, session_id: &SessionId, token: RegistrationToken, + ) -> bool { + self.restore_owned(session_id, token, None) + } + + /// Unregister the registration identified by `token`, handing the ID + /// back to `replaced` when that session is still running. + /// + /// Returns `false`, leaving the router unchanged, when `token` no longer + /// identifies the live registration. + pub(crate) fn restore_owned( + &self, + session_id: &SessionId, + token: RegistrationToken, + replaced: Option, ) -> bool { let mut sessions = self.sessions.lock(); - if sessions + if !sessions .get(session_id.as_str()) .is_some_and(|senders| senders.token == token) { - sessions.remove(session_id.as_str()); - true - } else { - false + return false; } + match replaced.filter(|replaced| !replaced.0.requests.is_closed()) { + Some(ReplacedRegistration(senders)) => { + sessions.insert(session_id.clone(), senders); + } + None => { + sessions.remove(session_id.as_str()); + } + } + true } /// Snapshot every currently-registered session ID. @@ -156,12 +194,13 @@ impl SessionRouter { /// Takes the notification broadcast and request channel from the client. /// If its request receiver was already taken by `take_request_rx()`, /// only notification routing is available. - pub(crate) fn ensure_started(&self, client: &crate::ClientInner) { + pub(crate) fn ensure_started(&self, client: &Arc) { let mut started = self.started.lock(); if *started { return; } *started = true; + let weak_client = Arc::downgrade(client); let extension_launch_provider = client.extension_launch_provider.clone(); let llm_inference = client.llm_inference.get().cloned(); let github_telemetry = client.on_github_telemetry.clone(); @@ -298,27 +337,44 @@ impl SessionRouter { .params .as_ref() .and_then(|p| p.get("sessionId")) - .and_then(|v| v.as_str()); + .and_then(|v| v.as_str()) + .map(str::to_owned); + let Some(session_id) = session_id else { + warn!(method = %request.method, "request missing sessionId"); + send_error( + &weak_client, + request.id, + error_codes::INVALID_PARAMS, + "missing required field: sessionId".to_string(), + ) + .await; + continue; + }; - if let Some(sid) = session_id { - let sender = { - let guard = sessions.lock(); - guard.get(sid).map(|s| s.requests.clone()) - }; - if let Some(sender) = sender { - let _ = sender.send(request); - } else { - warn!( - session_id = sid, - method = %request.method, - "request for unregistered session" - ); - } - } else { + let sender = sessions + .lock() + .get(session_id.as_str()) + .map(|s| s.requests.clone()); + let unrouted = match sender { + Some(sender) => sender.send(request).err().map(|error| error.0), + None => Some(request), + }; + // Every request owes the peer a response. Answer unroutable + // ones (for example, after a `Session` was dropped without + // detaching) so the runtime does not wait for its timeout. + if let Some(request) = unrouted { warn!( + session_id = %session_id, method = %request.method, - "request missing sessionId" + "request for unregistered session" ); + send_error( + &weak_client, + request.id, + error_codes::INTERNAL_ERROR, + format!("Session not found: {session_id}"), + ) + .await; } } }); @@ -326,36 +382,30 @@ impl SessionRouter { } } -#[cfg(test)] -mod tests { - use super::*; - - fn session_id() -> SessionId { - SessionId::new("router-ownership") - } - - #[test] - fn each_registration_gets_a_distinct_token() { - let router = SessionRouter::new(); - let first = router.register(&session_id()); - let second = router.register(&session_id()); - assert_ne!(first.token, second.token); - } - - #[test] - fn unregister_owned_removes_only_the_matching_registration() { - let router = SessionRouter::new(); - let stale = router.register(&session_id()); - let live = router.register(&session_id()); - - // The stale owner must not evict the registration that replaced it. - assert!(!router.unregister_owned(&session_id(), stale.token)); - assert_eq!(router.session_ids(), vec![session_id()]); - - assert!(router.unregister_owned(&session_id(), live.token)); - assert!(router.session_ids().is_empty()); - - // Removing twice is a no-op rather than evicting a future tenant. - assert!(!router.unregister_owned(&session_id(), live.token)); - } +async fn send_error( + client: &Weak, + request_id: u64, + code: i32, + message: String, +) { + let Some(inner) = client.upgrade() else { + return; + }; + // Retire any cancellation registered for a request that never reaches a handler. + drop(inner.rpc.cancellable_requests.claim(request_id)); + let _ = crate::Client::from_inner(inner) + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request_id, + result: None, + error: Some(JsonRpcError { + code, + message, + data: None, + }), + }) + .await; } + +#[cfg(test)] +mod tests; diff --git a/rust/src/router/tests.rs b/rust/src/router/tests.rs new file mode 100644 index 0000000000..f63ca005f7 --- /dev/null +++ b/rust/src/router/tests.rs @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +fn session_id() -> SessionId { + SessionId::new("router-ownership") +} + +#[test] +fn each_registration_gets_a_distinct_token() { + let router = SessionRouter::new(); + let first = router.register(&session_id()); + let second = router.register(&session_id()); + assert_ne!(first.token, second.token); +} + +#[test] +fn unregister_owned_removes_only_the_matching_registration() { + let router = SessionRouter::new(); + let stale = router.register(&session_id()); + let live = router.register(&session_id()); + + // The stale owner must not evict the registration that replaced it. + assert!(!router.unregister_owned(&session_id(), stale.token)); + assert_eq!(router.session_ids(), vec![session_id()]); + + assert!(router.unregister_owned(&session_id(), live.token)); + assert!(router.session_ids().is_empty()); + + // Removing twice is a no-op rather than evicting a future tenant. + assert!(!router.unregister_owned(&session_id(), live.token)); +} + +#[test] +fn restore_owned_hands_the_id_back_to_a_running_replaced_registration() { + let router = SessionRouter::new(); + let resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + assert!(replaced.is_some()); + + assert!(router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.is_registered_owner(&session_id(), resident.token)); +} + +#[test] +fn restore_owned_unregisters_when_the_replaced_session_has_stopped() { + let router = SessionRouter::new(); + let resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + drop(resident); + + assert!(router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.session_ids().is_empty()); +} + +#[test] +fn restore_owned_leaves_a_newer_registration_alone() { + let router = SessionRouter::new(); + let _resident = router.register(&session_id()); + let (attempt, replaced) = router.replace(&session_id()); + let newer = router.register(&session_id()); + + assert!(!router.restore_owned(&session_id(), attempt.token, replaced)); + assert!(router.is_registered_owner(&session_id(), newer.token)); +} diff --git a/rust/src/session.rs b/rust/src/session.rs index 82d1e2cc89..e13ee2309b 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -30,6 +30,7 @@ use crate::handler::{ use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; use crate::session_fs::SessionFsProvider; +use crate::skill_provider::SkillProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ @@ -37,12 +38,13 @@ use crate::types::{ CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride, SessionCapabilities, SessionConfig, SessionEvent, - SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, ToolResult, - ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names, + SessionId, SetModelOptions, SystemMessageConfig, Tool, ToolHandlerMap, ToolInvocation, + ToolResult, ToolResultExpanded, TraceContext, TranscriptRecovery, UiInputOptions, + ensure_attachment_display_names, take_tool_handlers, }; use crate::{ - Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification, - error_codes, + Client, Error, ErrorKind, JsonRpcResponse, ProtocolErrorKind, SessionErrorKind, + SessionEventNotification, error_codes, }; /// Fixed name of the runtime's built-in tool-search tool. A client can replace @@ -91,7 +93,10 @@ pub(crate) struct SessionHandlers { pub user_input: Option>, pub exit_plan_mode: Option>, pub auto_mode_switch: Option>, - pub tools: Arc>>, + pub skill_provider: Option>, + /// Shared with the owning [`Session`], which replaces the map when the + /// runtime accepts a [`Session::set_tools`] call. + pub tools: Arc>, } type PendingExternalTools = Arc>>>; @@ -259,6 +264,10 @@ struct PendingSessionRegistration { shutdown: CancellationToken, external_tools_shutdown: CancellationToken, startup_tasks: Arc, + /// The resident session a resume displaced. The runtime keeps that + /// session's bindings when the resume fails, so cleanup routes its + /// callbacks back to it; success drops it. + replaced: Option, disarmed: bool, } @@ -292,10 +301,19 @@ impl PendingSessionRegistration { shutdown, external_tools_shutdown, startup_tasks: Arc::new(StartupTasks::default()), + replaced: None, disarmed: false, } } + fn restoring_on_failure( + mut self, + replaced: Option, + ) -> Self { + self.replaced = replaced; + self + } + /// Guard for a registration whose session ID is assigned by the server. fn deferred( client: Client, @@ -309,6 +327,7 @@ impl PendingSessionRegistration { shutdown: shutdown.clone(), external_tools_shutdown, startup_tasks: Arc::new(StartupTasks::default()), + replaced: None, disarmed: false, }; let callback: crate::jsonrpc::InlineResponseCallback = Box::new(move |response| { @@ -355,7 +374,8 @@ impl PendingSessionRegistration { let _ = event_loop.await; if let Some(id) = self.registered_id() { if let PendingSessionId::Known(_, token) = self.session_id { - self.client.unregister_session_owned(&id, token); + self.client + .restore_session_owned(&id, token, self.replaced.take()); } else if let PendingSessionId::Deferred(stash) = &self.session_id && let Some((id, registration)) = stash.lock().as_ref() { @@ -367,6 +387,7 @@ impl PendingSessionRegistration { fn disarm(&mut self) { self.startup_tasks.disarm(); + self.replaced = None; self.disarmed = true; } } @@ -377,7 +398,8 @@ impl Drop for PendingSessionRegistration { self.cancel(); if let Some(id) = self.registered_id() { if let PendingSessionId::Known(_, token) = self.session_id { - self.client.unregister_session_owned(&id, token); + self.client + .restore_session_owned(&id, token, self.replaced.take()); } else if let PendingSessionId::Deferred(stash) = &self.session_id && let Some((id, registration)) = stash.lock().as_ref() { @@ -490,6 +512,7 @@ impl CreateEventLoop { /// unregisters from the router as a best-effort safety net. pub struct Session { ahp_creation_config: ParkingLotMutex>, + transcript_recovery: Option, id: SessionId, cwd: PathBuf, workspace_path: Option, @@ -517,6 +540,13 @@ pub struct Session { /// Cancels only host-owned external tool callbacks. Disconnect signals this /// before the destroy RPC without stopping unrelated event delivery. external_tools_shutdown: CancellationToken, + /// This client's tool handlers, shared with the event loop's + /// `external_tool.requested` dispatch. + tool_handlers: Arc>, + /// Held for each [`Session::set_tools`] request until the runtime answers, + /// so replacements reach the runtime, and replace the handlers, one at a + /// time. + set_tools_lock: Arc>, /// Only populated while a `send_and_wait` call is in flight. /// /// Sync `parking_lot::Mutex` because the lock is never held across an @@ -594,6 +624,11 @@ impl Session { self.workspace_path.as_deref() } + /// Transcript repair proposed when this session was resumed, if any. + pub fn transcript_recovery(&self) -> Option<&TranscriptRecovery> { + self.transcript_recovery.as_ref() + } + /// Remote session URL, if the session is running remotely. pub fn remote_url(&self) -> Option<&str> { self.remote_url.as_deref() @@ -1126,6 +1161,115 @@ impl Session { .await } + /// Replace the tools this client supplies to the session. + /// + /// `tools` becomes the complete set of externally implemented tools this + /// client supplies, replacing the set from [`SessionConfig::with_tools`], + /// [`ResumeSessionConfig::with_tools`], or a previous call. Built-in, MCP, + /// plugin, and extension tools, and tools that other clients connected to + /// the session supply, are unaffected. Pass an empty collection to remove + /// all of this client's tools. + /// + /// As at startup, this session dispatches calls to tools that carry a + /// [handler](Tool::with_handler), and advertises declaration-only tools for + /// another client to service. + /// + /// The new handlers take effect as soon as the runtime accepts the + /// replacement: from then on, every tool request this session dispatches + /// uses them, including requests the runtime sent before it accepted. + /// Calls already running finish on the handlers that started them. If the + /// runtime rejects the replacement, nothing changes. Concurrent calls on + /// the same session are applied one at a time, in the order they start. + /// + /// The runtime offers the new tools from the agent's next model request, + /// which can fall within a turn in progress. A model request already in + /// flight was made with the previous tools, so the agent can still call a + /// tool you removed. This session doesn't answer that call, and it can stay + /// pending until the turn is aborted. If a running turn might still call a + /// tool you remove, replace tools while the session is idle. + /// + /// **Experimental.** Wraps the experimental `session.tools.set` RPC, which + /// requires a runtime that supports it. + /// + /// # Errors + /// + /// Returns [`ErrorKind::InvalidConfig`], without contacting the runtime, if + /// two tools carry handlers under the same name. The runtime rejects invalid + /// tool names and names that another connected client already supplies. + /// + /// # Cancel safety + /// + /// **Cancel-safe.** Dropping this future while an earlier replacement is + /// still in flight sends nothing. Once the request starts, it runs to + /// completion in its own task. Dropping this future then doesn't abandon + /// the replacement: an accepted replacement still takes effect, and later + /// calls still wait for the runtime's answer. Only the result is lost. + /// + /// # Example + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use github_copilot_sdk::Tool; + /// # use github_copilot_sdk::tool::ToolHandler; + /// # async fn example( + /// # session: github_copilot_sdk::session::Session, + /// # search_issues: Arc, + /// # ) -> Result<(), github_copilot_sdk::Error> { + /// session + /// .set_tools([Tool::new("search_issues") + /// .with_description("Search the issues shown on the current page") + /// .with_handler(search_issues)]) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn set_tools>(&self, tools: I) -> Result<(), Error> { + let mut tools: Vec = tools.into_iter().collect(); + let handlers = take_tool_handlers(&mut tools)?; + let mut params = serde_json::json!({ "sessionId": self.id }); + params["tools"] = serde_json::to_value(&tools)?; + + let set_tools_lock = self.set_tools_lock.clone().lock_owned().await; + let installed = self.tool_handlers.clone(); + let replaced = Arc::new(ParkingLotMutex::new(None)); + // Swapping on the read task means no tool request read after the + // runtime accepted is dispatched to the previous handlers. + let accepted: crate::jsonrpc::InlineResponseCallback = Box::new({ + let replaced = replaced.clone(); + move |_| { + let previous = std::mem::replace(&mut *installed.write(), handlers); + *replaced.lock() = Some(previous); + Ok(()) + } + }); + let request = self.client.call_with_inline_callback( + rpc_methods::SESSION_TOOLS_SET, + Some(params), + Some(accepted), + ); + let span = tracing::error_span!("set_tools", session_id = %self.id); + // The task owns the request and the lock, so even if this future is + // dropped, an accepted replacement still takes effect and the next + // call waits for the runtime's answer. + let replacement = tokio::spawn( + async move { + let _set_tools_lock = set_tools_lock; + let result = request.await; + // Run the replaced handlers' destructors here rather than on + // the read task, which every session on this client shares. + let previous = replaced.lock().take(); + drop(previous); + result + } + .instrument(span), + ); + match replacement.await { + Ok(result) => result.map(|_| ()), + Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()), + Err(_) => Err(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled).into()), + } + } + /// Disconnect this session from the CLI. /// /// Sends the `session.detach` RPC, stops the event loop, and unregisters @@ -1533,6 +1677,12 @@ impl Client { shutdown: CancellationToken, ) -> Result { let total_start = Instant::now(); + if config.cloud.is_some() && config.skill_provider.is_some() { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "Skill providers are not supported for cloud sessions.", + )); + } let ahp_creation_config = if self .inner .ahp_host_sessions @@ -1627,6 +1777,9 @@ impl Client { runtime.permission_handler.take(), runtime.permission_policy.take(), ); + let tool_handlers = Arc::new(parking_lot::RwLock::new(std::mem::take( + &mut runtime.tool_handlers, + ))); let handlers = SessionHandlers { permission: permission_handler, managed_settings_enabled: has_managed_settings( @@ -1638,7 +1791,8 @@ impl Client { user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), - tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)), + skill_provider: runtime.skill_provider.take(), + tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); let transforms = runtime.system_message_transform.take(); @@ -1670,6 +1824,15 @@ impl Client { does not implement SessionFsSqliteProvider", )); } + if self.inner.session_fs_binary_declared + && let Some(ref provider) = session_fs_provider + && provider.binary().is_none() + { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "SessionFs capabilities declare binary support but the provider does not implement SessionFsBinaryProvider", + )); + } let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; @@ -1833,6 +1996,7 @@ impl Client { ); let session = Session { ahp_creation_config: ParkingLotMutex::new(ahp_creation_config), + transcript_recovery: None, id: session_id, cwd: self.cwd().clone(), workspace_path: create_result.workspace_path, @@ -1841,6 +2005,8 @@ impl Client { event_loop: ParkingLotMutex::new(Some(event_loop)), shutdown, external_tools_shutdown, + tool_handlers, + set_tools_lock: Arc::default(), idle_waiter, capabilities, open_canvases, @@ -1877,6 +2043,7 @@ impl Client { shutdown: CancellationToken, ) -> Result { let total_start = Instant::now(); + let mode = self.inner.mode; let ahp_creation_config = if self .inner .ahp_host_sessions @@ -1894,7 +2061,6 @@ impl Client { if let Some(transforms) = config.system_message_transform.clone() { inject_transform_sections_resume(&mut config, transforms.as_ref()); } - let mode = self.inner.mode; if mode == crate::ClientMode::Empty && config.available_tools.is_none() { return Err(Error::with_message( ErrorKind::InvalidConfig, @@ -1957,6 +2123,9 @@ impl Client { runtime.permission_handler.take(), runtime.permission_policy.take(), ); + let tool_handlers = Arc::new(parking_lot::RwLock::new(std::mem::take( + &mut runtime.tool_handlers, + ))); let handlers = SessionHandlers { permission: permission_handler, managed_settings_enabled: has_managed_settings( @@ -1968,7 +2137,8 @@ impl Client { user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), - tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)), + skill_provider: runtime.skill_provider.take(), + tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); let transforms = runtime.system_message_transform.take(); @@ -2000,6 +2170,15 @@ impl Client { does not implement SessionFsSqliteProvider", )); } + if self.inner.session_fs_binary_declared + && let Some(ref provider) = session_fs_provider + && provider.binary().is_none() + { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "SessionFs capabilities declare binary support but the provider does not implement SessionFsBinaryProvider", + )); + } let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; @@ -2010,7 +2189,7 @@ impl Client { // Active prepared subscribers retain their existing bounded delivery. let resume_bootstrap = (event_tx.receiver_count() == 0) .then(|| crate::subscription::ResumeBootstrap::new(&event_tx)); - let registration = self.register_session(&session_id); + let (registration, replaced) = self.replace_session_registration(&session_id); let registration_token = registration.token; let channels = registration.channels; let idle_waiter = Arc::new(ParkingLotMutex::new(None)); @@ -2022,7 +2201,8 @@ impl Client { registration_token, shutdown.clone(), external_tools_shutdown.clone(), - ); + ) + .restoring_on_failure(replaced); let event_loop = spawn_event_loop( session_id.clone(), Client::from_inner(self.inner.clone()), @@ -2133,6 +2313,7 @@ impl Client { ); let session = Session { ahp_creation_config: ParkingLotMutex::new(ahp_creation_config), + transcript_recovery: resume_result.transcript_recovery, id: session_id, cwd: self.cwd().clone(), workspace_path: resume_result.workspace_path, @@ -2141,6 +2322,8 @@ impl Client { event_loop: ParkingLotMutex::new(Some(event_loop)), shutdown, external_tools_shutdown, + tool_handlers, + set_tools_lock: Arc::default(), idle_waiter, capabilities, open_canvases, @@ -2927,7 +3110,7 @@ async fn handle_notification( let tool_handler = if data.tool_name.is_empty() { None } else { - handlers.tools.get(&data.tool_name).cloned() + handlers.tools.read().get(&data.tool_name).cloned() }; let Some(tool_handler) = tool_handler else { return; @@ -3376,6 +3559,110 @@ struct RequestDispatchContext<'a> { bearer_token_providers: &'a HashMap>, } +/// Serve one `skillProvider.*` callback. Provider failures are reported +/// generically so their details never reach the runtime or the model. +/// +/// A `$/cancelRequest` for this request, or the connection closing, drops the +/// provider future. +async fn handle_skill_provider_request( + client: &Client, + session_id: &SessionId, + provider: Option<&Arc>, + request: &crate::JsonRpcRequest, +) { + let Some(pending) = client.inner.rpc.cancellable_requests.claim(request.id) else { + // The connection closed before dispatch, so nobody awaits a response. + return; + }; + let Some(provider) = provider else { + let _ = send_error_response( + client, + request.id, + error_codes::INTERNAL_ERROR, + &format!("No skill provider for session: {session_id}"), + ) + .await; + return; + }; + + let read_name = if request.method == rpc_methods::SKILLPROVIDER_READ { + let Some(name) = request + .params + .as_ref() + .and_then(|params| params.get("name")) + .and_then(Value::as_str) + else { + let _ = send_error_response( + client, + request.id, + error_codes::INVALID_PARAMS, + "missing required field: name", + ) + .await; + return; + }; + Some(name) + } else { + None + }; + let operation = if read_name.is_some() { + "readSkill" + } else { + "listSkills" + }; + let call = async { + match read_name { + Some(name) => provider + .read_skill(name) + .await + .map(|markdown| serde_json::json!({ "markdown": markdown })), + None => provider + .list_skills() + .await + .map(|skills| serde_json::json!({ "skills": skills })), + } + }; + + let connection_closed = client.inner.rpc.connection_closed_token(); + let result = tokio::select! { + biased; + _ = connection_closed.cancelled() => return, + _ = pending.cancellation().cancelled() => { + let _ = send_error_response( + client, + request.id, + error_codes::REQUEST_CANCELLED, + &format!("Skill provider {operation} cancelled"), + ) + .await; + return; + } + result = call => result, + }; + + match result { + Ok(result) => { + let response = JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: Some(result), + error: None, + }; + let _ = client.send_response(&response).await; + } + Err(error) => { + warn!(error = %error, operation, "skill provider callback failed"); + let _ = send_error_response( + client, + request.id, + error_codes::INTERNAL_ERROR, + &format!("Skill provider {operation} failed"), + ) + .await; + } + } +} + /// Process a JSON-RPC request from the CLI. async fn handle_request( session_id: &SessionId, @@ -3406,6 +3693,14 @@ async fn handle_request( return; } + if request.method == rpc_methods::SKILLPROVIDER_LIST + || request.method == rpc_methods::SKILLPROVIDER_READ + { + handle_skill_provider_request(client, &sid, handlers.skill_provider.as_ref(), &request) + .await; + return; + } + match request.method.as_str() { "hooks.invoke" => { let params = request.params.as_ref(); @@ -3689,311 +3984,7 @@ fn inject_transform_sections_resume( } #[cfg(test)] -mod tests { - use serde_json::json; - - use super::{ - build_mode_post_create_patch, has_managed_settings, is_autopilot_continuation_idle, - permission_request_data, permission_response_params, - }; - use crate::handler::PermissionResult; - use crate::types::{ - PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, - PermissionDecisionSurface, RequestId, SessionEvent, SessionId, - }; - - #[test] - fn identifies_only_autopilot_continuation_idles() { - let mut event = SessionEvent { - id: "event-1".to_string(), - timestamp: "2026-01-01T00:00:00Z".to_string(), - parent_id: None, - ephemeral: None, - agent_id: None, - debug_cli_received_at_ms: None, - debug_ws_forwarded_at_ms: None, - event_type: "session.idle".to_string(), - data: json!({ "mode": "autopilot" }), - }; - - assert!(is_autopilot_continuation_idle(&event)); - - event.data = json!({ "mode": "interactive" }); - assert!(!is_autopilot_continuation_idle(&event)); - - event.data = json!({}); - assert!(!is_autopilot_continuation_idle(&event)); - } - - #[test] - fn empty_mode_post_patch_sets_empty_included_builtin_skills() { - let patch = - build_mode_post_create_patch(crate::ClientMode::Empty, None, None, None, None, None) - .expect("empty mode always sends a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(Vec::new()), - "empty mode must fail closed with an empty includedBuiltinSkills list" - ); - assert_eq!(patch.installed_plugins.as_ref().map(|p| p.len()), Some(0)); - // Serializes as an explicit empty array (not omitted). - let value = serde_json::to_value(&patch).expect("serialize patch"); - assert_eq!(value["includedBuiltinSkills"], serde_json::json!([])); - } - - #[test] - fn empty_mode_post_patch_preserves_explicit_builtin_skill_allowlist() { - let patch = build_mode_post_create_patch( - crate::ClientMode::Empty, - Some(false), - Some(false), - Some(true), - Some(true), - Some(vec!["code-review".to_string()]), - ) - .expect("empty mode always sends a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(vec!["code-review".to_string()]) - ); - } - - #[test] - fn copilot_cli_mode_does_not_inject_included_builtin_skills() { - // No fields set -> no patch at all. - assert!( - build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - None, - None, - None, - None, - None - ) - .is_none() - ); - // A field set -> patch sent, but skills field stays absent. - let patch = build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - Some(true), - None, - None, - None, - None, - ) - .expect("a set field triggers a patch"); - assert_eq!(patch.included_builtin_skills, None); - assert!(patch.installed_plugins.is_none()); - let value = serde_json::to_value(&patch).expect("serialize patch"); - assert!(value.get("includedBuiltinSkills").is_none()); - - let patch = build_mode_post_create_patch( - crate::ClientMode::CopilotCli, - None, - None, - None, - None, - Some(vec!["code-review".to_string()]), - ) - .expect("an explicit allowlist triggers a patch"); - assert_eq!( - patch.included_builtin_skills, - Some(vec!["code-review".to_string()]) - ); - } - - #[test] - fn direct_injection_enables_managed_safeguards() { - let settings = crate::types::ManagedSettings::default(); - assert!(has_managed_settings(None, Some(&settings))); - assert!(!has_managed_settings(None, None)); - } - - fn attribution_context() -> PermissionDecisionContext { - PermissionDecisionContext { - outcome: PermissionDecisionOutcome::AutoApproved, - response_capability: None, - source: PermissionDecisionSource::AssistedApproval, - surface: PermissionDecisionSurface::CopilotApp, - } - } - - #[test] - fn response_params_omit_decision_context_without_attribution() { - for (result, expected) in [ - ( - PermissionResult::approve_once(), - json!({ "kind": "approve-once" }), - ), - (PermissionResult::reject(None), json!({ "kind": "reject" })), - ( - PermissionResult::reject(Some("bad".to_string())), - json!({ "kind": "reject", "feedback": "bad" }), - ), - ( - PermissionResult::user_not_available(), - json!({ "kind": "user-not-available" }), - ), - ] { - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &result, - ) - .unwrap(); - assert_eq!( - params, - json!({ - "sessionId": "session-1", - "requestId": "permission-1", - "result": expected, - }) - ); - } - } - - #[test] - fn response_params_forward_decision_context_alongside_result() { - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &PermissionResult::approve_once().with_context(attribution_context()), - ) - .unwrap(); - assert_eq!( - params, - json!({ - "sessionId": "session-1", - "requestId": "permission-1", - "result": { "kind": "approve-once" }, - "decisionContext": { - "outcome": "auto_approved", - "source": "assisted_approval", - "surface": "copilot_app", - }, - }) - ); - // The context is a sibling of `result`, never nested inside it. - assert!(params["result"].get("decisionContext").is_none()); - } - - #[test] - fn response_params_suppressed_for_no_result() { - assert!( - permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &PermissionResult::NoResult, - ) - .is_none() - ); - } - - #[test] - fn with_context_is_a_no_op_on_no_result() { - let result = PermissionResult::no_result().with_context(attribution_context()); - assert!(matches!(result, PermissionResult::NoResult)); - } - - #[test] - fn with_context_replaces_rather_than_nests() { - let result = PermissionResult::approve_once() - .with_context(attribution_context()) - .with_context(PermissionDecisionContext { - outcome: PermissionDecisionOutcome::PromptedUser, - response_capability: None, - source: PermissionDecisionSource::HumanResponse, - surface: PermissionDecisionSurface::Sdk, - }); - let params = permission_response_params( - &SessionId::from("session-1"), - &RequestId::from("permission-1"), - &result, - ) - .unwrap(); - assert_eq!( - params["decisionContext"], - json!({ - "outcome": "prompted_user", - "source": "human_response", - "surface": "sdk", - }) - ); - } - - #[test] - fn permission_request_data_reads_nested_managed_approval_metadata() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": true, - "path": "/workspace/file.txt" - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(true)); - assert_eq!( - data.extra["permissionRequest"]["path"], - "/workspace/file.txt" - ); - } - - #[test] - fn permission_request_data_preserves_managed_flag_when_other_fields_are_malformed() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": true, - "toolCallId": 42 - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(true)); - assert_eq!(data.extra["requestId"], "permission-1"); - } - - #[test] - fn permission_request_data_fails_closed_for_malformed_managed_flag() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": "yes", - "path": "/workspace/file.txt" - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(true)); - } - - #[test] - fn permission_request_data_preserves_valid_false_managed_flag() { - let data = permission_request_data( - &json!({ - "requestId": "permission-1", - "permissionRequest": { - "kind": "read", - "managedApprovalRequired": false, - "path": "/workspace/file.txt" - } - }), - false, - ); - - assert_eq!(data.managed_approval_required, Some(false)); - } -} +mod tests; #[cfg(test)] mod startup_tasks_tests; diff --git a/rust/src/session/startup_tasks_tests.rs b/rust/src/session/startup_tasks_tests.rs index bdec2551ad..5bc14f3287 100644 --- a/rust/src/session/startup_tasks_tests.rs +++ b/rust/src/session/startup_tasks_tests.rs @@ -68,6 +68,7 @@ async fn check_aborted_nested_dispatches(nested_first: bool) { user_input: None, exit_plan_mode: None, auto_mode_switch: None, + skill_provider: None, tools: Arc::new(Default::default()), }; let startup_tasks = Arc::new(StartupTasks::default()); diff --git a/rust/src/session/tests.rs b/rust/src/session/tests.rs new file mode 100644 index 0000000000..e184787d47 --- /dev/null +++ b/rust/src/session/tests.rs @@ -0,0 +1,302 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use serde_json::json; + +use super::{ + build_mode_post_create_patch, has_managed_settings, is_autopilot_continuation_idle, + permission_request_data, permission_response_params, +}; +use crate::handler::PermissionResult; +use crate::types::{ + PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, + PermissionDecisionSurface, RequestId, SessionEvent, SessionId, +}; + +#[test] +fn identifies_only_autopilot_continuation_idles() { + let mut event = SessionEvent { + id: "event-1".to_string(), + timestamp: "2026-01-01T00:00:00Z".to_string(), + parent_id: None, + ephemeral: None, + agent_id: None, + debug_cli_received_at_ms: None, + debug_ws_forwarded_at_ms: None, + event_type: "session.idle".to_string(), + data: json!({ "mode": "autopilot" }), + }; + + assert!(is_autopilot_continuation_idle(&event)); + + event.data = json!({ "mode": "interactive" }); + assert!(!is_autopilot_continuation_idle(&event)); + + event.data = json!({}); + assert!(!is_autopilot_continuation_idle(&event)); +} + +#[test] +fn empty_mode_post_patch_sets_empty_included_builtin_skills() { + let patch = + build_mode_post_create_patch(crate::ClientMode::Empty, None, None, None, None, None) + .expect("empty mode always sends a patch"); + assert_eq!( + patch.included_builtin_skills, + Some(Vec::new()), + "empty mode must fail closed with an empty includedBuiltinSkills list" + ); + assert_eq!(patch.installed_plugins.as_ref().map(|p| p.len()), Some(0)); + // Serializes as an explicit empty array (not omitted). + let value = serde_json::to_value(&patch).expect("serialize patch"); + assert_eq!(value["includedBuiltinSkills"], serde_json::json!([])); +} + +#[test] +fn empty_mode_post_patch_preserves_explicit_builtin_skill_allowlist() { + let patch = build_mode_post_create_patch( + crate::ClientMode::Empty, + Some(false), + Some(false), + Some(true), + Some(true), + Some(vec!["code-review".to_string()]), + ) + .expect("empty mode always sends a patch"); + assert_eq!( + patch.included_builtin_skills, + Some(vec!["code-review".to_string()]) + ); +} + +#[test] +fn copilot_cli_mode_does_not_inject_included_builtin_skills() { + // No fields set -> no patch at all. + assert!( + build_mode_post_create_patch(crate::ClientMode::CopilotCli, None, None, None, None, None) + .is_none() + ); + // A field set -> patch sent, but skills field stays absent. + let patch = build_mode_post_create_patch( + crate::ClientMode::CopilotCli, + Some(true), + None, + None, + None, + None, + ) + .expect("a set field triggers a patch"); + assert_eq!(patch.included_builtin_skills, None); + assert!(patch.installed_plugins.is_none()); + let value = serde_json::to_value(&patch).expect("serialize patch"); + assert!(value.get("includedBuiltinSkills").is_none()); + + let patch = build_mode_post_create_patch( + crate::ClientMode::CopilotCli, + None, + None, + None, + None, + Some(vec!["code-review".to_string()]), + ) + .expect("an explicit allowlist triggers a patch"); + assert_eq!( + patch.included_builtin_skills, + Some(vec!["code-review".to_string()]) + ); +} + +#[test] +fn direct_injection_enables_managed_safeguards() { + let settings = crate::types::ManagedSettings::default(); + assert!(has_managed_settings(None, Some(&settings))); + assert!(!has_managed_settings(None, None)); +} + +fn attribution_context() -> PermissionDecisionContext { + PermissionDecisionContext { + outcome: PermissionDecisionOutcome::AutoApproved, + response_capability: None, + source: PermissionDecisionSource::AssistedApproval, + surface: PermissionDecisionSurface::CopilotApp, + } +} + +#[test] +fn response_params_omit_decision_context_without_attribution() { + for (result, expected) in [ + ( + PermissionResult::approve_once(), + json!({ "kind": "approve-once" }), + ), + (PermissionResult::reject(None), json!({ "kind": "reject" })), + ( + PermissionResult::reject(Some("bad".to_string())), + json!({ "kind": "reject", "feedback": "bad" }), + ), + ( + PermissionResult::user_not_available(), + json!({ "kind": "user-not-available" }), + ), + ] { + let params = permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &result, + ) + .unwrap(); + assert_eq!( + params, + json!({ + "sessionId": "session-1", + "requestId": "permission-1", + "result": expected, + }) + ); + } +} + +#[test] +fn response_params_forward_decision_context_alongside_result() { + let params = permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &PermissionResult::approve_once().with_context(attribution_context()), + ) + .unwrap(); + assert_eq!( + params, + json!({ + "sessionId": "session-1", + "requestId": "permission-1", + "result": { "kind": "approve-once" }, + "decisionContext": { + "outcome": "auto_approved", + "source": "assisted_approval", + "surface": "copilot_app", + }, + }) + ); + // The context is a sibling of `result`, never nested inside it. + assert!(params["result"].get("decisionContext").is_none()); +} + +#[test] +fn response_params_suppressed_for_no_result() { + assert!( + permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &PermissionResult::NoResult, + ) + .is_none() + ); +} + +#[test] +fn with_context_is_a_no_op_on_no_result() { + let result = PermissionResult::no_result().with_context(attribution_context()); + assert!(matches!(result, PermissionResult::NoResult)); +} + +#[test] +fn with_context_replaces_rather_than_nests() { + let result = PermissionResult::approve_once() + .with_context(attribution_context()) + .with_context(PermissionDecisionContext { + outcome: PermissionDecisionOutcome::PromptedUser, + response_capability: None, + source: PermissionDecisionSource::HumanResponse, + surface: PermissionDecisionSurface::Sdk, + }); + let params = permission_response_params( + &SessionId::from("session-1"), + &RequestId::from("permission-1"), + &result, + ) + .unwrap(); + assert_eq!( + params["decisionContext"], + json!({ + "outcome": "prompted_user", + "source": "human_response", + "surface": "sdk", + }) + ); +} + +#[test] +fn permission_request_data_reads_nested_managed_approval_metadata() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": true, + "path": "/workspace/file.txt" + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(true)); + assert_eq!( + data.extra["permissionRequest"]["path"], + "/workspace/file.txt" + ); +} + +#[test] +fn permission_request_data_preserves_managed_flag_when_other_fields_are_malformed() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": true, + "toolCallId": 42 + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(true)); + assert_eq!(data.extra["requestId"], "permission-1"); +} + +#[test] +fn permission_request_data_fails_closed_for_malformed_managed_flag() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": "yes", + "path": "/workspace/file.txt" + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(true)); +} + +#[test] +fn permission_request_data_preserves_valid_false_managed_flag() { + let data = permission_request_data( + &json!({ + "requestId": "permission-1", + "permissionRequest": { + "kind": "read", + "managedApprovalRequired": false, + "path": "/workspace/file.txt" + } + }), + false, + ); + + assert_eq!(data.managed_approval_required, Some(false)); +} diff --git a/rust/src/session_fs.rs b/rust/src/session_fs.rs index 1d96daae9e..b65a0d883d 100644 --- a/rust/src/session_fs.rs +++ b/rust/src/session_fs.rs @@ -65,6 +65,8 @@ use crate::{Custom, Repr}; pub struct SessionFsCapabilities { /// Whether the provider supports SQLite query/exists operations. pub sqlite: bool, + /// Whether the provider supports exact binary reads and writes. + pub binary: bool, } impl SessionFsCapabilities { @@ -78,6 +80,12 @@ impl SessionFsCapabilities { self.sqlite = sqlite; self } + + /// Enable binary file reads and writes. + pub fn with_binary(mut self, binary: bool) -> Self { + self.binary = binary; + self + } } /// Configuration for a custom session filesystem provider. @@ -175,6 +183,7 @@ impl fmt::Display for FsErrorKind { #[derive(Debug)] pub struct FsError { repr: Repr, + write_changed: bool, } impl FsError { @@ -188,6 +197,7 @@ impl FsError { kind, error: error.into(), }), + write_changed: false, } } @@ -216,7 +226,24 @@ impl FsError { { Self { repr: Repr::SimpleMessage(kind, message.into()), + write_changed: false, + } + } + + /// Mark a failed `write_file` as having changed its target before failing. + #[must_use] + pub fn with_write_changed(mut self) -> Self { + self.write_changed = true; + self + } + + pub(crate) fn into_write_wire(self) -> SessionFsError { + let changed = self.write_changed; + let mut wire = self.into_wire(); + if changed { + wire.write_changed = Some(true); } + wire } pub(crate) fn into_wire(self) -> SessionFsError { @@ -224,10 +251,12 @@ impl FsError { FsErrorKind::NotFound(message) => SessionFsError { code: SessionFsErrorCode::ENOENT, message: Some(message.clone()), + write_changed: None, }, FsErrorKind::Other => SessionFsError { code: SessionFsErrorCode::UNKNOWN, message: Some(self.to_string()), + write_changed: None, }, } } @@ -256,6 +285,7 @@ impl From for FsError { fn from(kind: FsErrorKind) -> Self { Self { repr: Repr::Simple(kind), + write_changed: false, } } } @@ -402,7 +432,13 @@ pub trait SessionFsProvider: Send + Sync + 'static { )) } - /// Write content to a file, creating parent directories if needed. + /// Return an optional binary implementation when this provider supports it. + fn binary(&self) -> Option<&dyn SessionFsBinaryProvider> { + None + } + + /// Write content to a file, creating parent directories if needed. Mark a failed + /// write with [`FsError::with_write_changed`] only if it changed the target. async fn write_file( &self, path: &str, @@ -505,6 +541,21 @@ pub trait SessionFsProvider: Send + Sync + 'static { } } +/// Optional exact-byte reads and writes for providers declaring `binary`. +#[async_trait] +pub trait SessionFsBinaryProvider: Send + Sync { + /// Read exact bytes of the file at `path`. + async fn read_file_bytes(&self, path: &str) -> Result, FsError>; + + /// Write exact bytes to the file at `path`, using `mode` when supplied. + async fn write_file_bytes( + &self, + path: &str, + content: &[u8], + mode: Option, + ) -> Result<(), FsError>; +} + /// Optional trait for providers that support SQLite operations. /// /// Providers are already session-scoped (created per session by the factory), @@ -621,52 +672,7 @@ pub struct SessionFsSqliteQueryResult { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn fs_error_maps_io_not_found_to_enoent() { - let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt"); - let fs_err: FsError = io_err.into(); - assert!( - matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt") - ); - let wire = fs_err.into_wire(); - assert_eq!(wire.code, SessionFsErrorCode::ENOENT); - } - - #[test] - fn fs_error_maps_other_io_to_unknown() { - let io_err = std::io::Error::other("disk full"); - let fs_err: FsError = io_err.into(); - assert!(matches!(fs_err.kind(), FsErrorKind::Other)); - let wire = fs_err.into_wire(); - assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN); - assert!(wire.message.unwrap().contains("disk full")); - } - - #[test] - fn conventions_maps_to_wire() { - assert_eq!( - SessionFsConventions::Posix.into_wire(), - SessionFsSetProviderConventions::Posix - ); - assert_eq!( - SessionFsConventions::Windows.into_wire(), - SessionFsSetProviderConventions::Windows - ); - } - - struct DefaultProvider; - #[async_trait] - impl SessionFsProvider for DefaultProvider {} - - #[tokio::test] - async fn default_impls_return_unsupported() { - let p = DefaultProvider; - let err = p.read_file("/x").await.unwrap_err(); - assert!( - matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported") - ); - } -} +mod tests; + +#[cfg(test)] +mod write_failure_tests; diff --git a/rust/src/session_fs/tests.rs b/rust/src/session_fs/tests.rs new file mode 100644 index 0000000000..7736636d02 --- /dev/null +++ b/rust/src/session_fs/tests.rs @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn fs_error_maps_io_not_found_to_enoent() { + let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt"); + let fs_err: FsError = io_err.into(); + assert!(matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt")); + let wire = fs_err.into_wire(); + assert_eq!(wire.code, SessionFsErrorCode::ENOENT); +} + +#[test] +fn fs_error_maps_other_io_to_unknown() { + let io_err = std::io::Error::other("disk full"); + let fs_err: FsError = io_err.into(); + assert!(matches!(fs_err.kind(), FsErrorKind::Other)); + let wire = fs_err.into_wire(); + assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN); + assert!(wire.message.unwrap().contains("disk full")); +} + +#[test] +fn conventions_maps_to_wire() { + assert_eq!( + SessionFsConventions::Posix.into_wire(), + SessionFsSetProviderConventions::Posix + ); + assert_eq!( + SessionFsConventions::Windows.into_wire(), + SessionFsSetProviderConventions::Windows + ); +} + +struct DefaultProvider; +#[async_trait] +impl SessionFsProvider for DefaultProvider {} + +#[tokio::test] +async fn default_impls_return_unsupported() { + let p = DefaultProvider; + let err = p.read_file("/x").await.unwrap_err(); + assert!(matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported")); +} diff --git a/rust/src/session_fs/write_failure_tests.rs b/rust/src/session_fs/write_failure_tests.rs new file mode 100644 index 0000000000..905781a0db --- /dev/null +++ b/rust/src/session_fs/write_failure_tests.rs @@ -0,0 +1,26 @@ +#![cfg(test)] + +use super::{FsError, FsErrorKind, SessionFsErrorCode}; + +#[test] +fn failed_write_reports_only_known_changes() { + let ordinary = FsError::with_message(FsErrorKind::Other, "rejected").into_write_wire(); + assert_eq!(ordinary.code, SessionFsErrorCode::UNKNOWN); + assert_eq!(ordinary.write_changed, None); + + let changed = FsError::with_message(FsErrorKind::Other, "disk full") + .with_write_changed() + .into_write_wire(); + assert_eq!(changed.message.as_deref(), Some("disk full")); + assert_eq!(changed.write_changed, Some(true)); + let json = serde_json::to_value(changed).unwrap(); + assert_eq!(json["writeChanged"], true); +} + +#[test] +fn write_marker_does_not_leak_into_other_methods() { + let read_error = FsError::with_message(FsErrorKind::Other, "read failed") + .with_write_changed() + .into_wire(); + assert_eq!(read_error.write_changed, None); +} diff --git a/rust/src/session_fs_dispatch.rs b/rust/src/session_fs_dispatch.rs index c84981ac0c..06cbb016b9 100644 --- a/rust/src/session_fs_dispatch.rs +++ b/rust/src/session_fs_dispatch.rs @@ -7,25 +7,29 @@ use std::sync::Arc; +use base64::Engine as _; use serde::Serialize; use serde_json::Value; use tracing::warn; use crate::generated::api_types::{ SessionFsAppendFileRequest, SessionFsError, SessionFsErrorCode, SessionFsExistsRequest, - SessionFsExistsResult, SessionFsMkdirRequest, SessionFsReadFileRequest, - SessionFsReadFileResult, SessionFsReaddirRequest, SessionFsReaddirResult, - SessionFsReaddirWithTypesRequest, SessionFsReaddirWithTypesResult, SessionFsRenameRequest, - SessionFsRmRequest, SessionFsSqliteExistsParams, SessionFsSqliteExistsResult, - SessionFsSqliteQueryRequest, SessionFsSqliteQueryResult as GeneratedSqliteQueryResult, + SessionFsExistsResult, SessionFsMkdirRequest, SessionFsReadFileBytesRequest, + SessionFsReadFileBytesResult, SessionFsReadFileRequest, SessionFsReadFileResult, + SessionFsReaddirRequest, SessionFsReaddirResult, SessionFsReaddirWithTypesRequest, + SessionFsReaddirWithTypesResult, SessionFsRenameRequest, SessionFsRmRequest, + SessionFsSqliteExistsParams, SessionFsSqliteExistsResult, SessionFsSqliteQueryRequest, + SessionFsSqliteQueryResult as GeneratedSqliteQueryResult, SessionFsSqliteTransactionError as GeneratedSqliteTransactionError, SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionRequest, SessionFsSqliteTransactionResult as GeneratedSqliteTransactionResult, SessionFsStatRequest, - SessionFsStatResult, SessionFsWriteFileRequest, + SessionFsStatResult, SessionFsWriteFileBytesRequest, SessionFsWriteFileRequest, }; use crate::session_fs::SessionFsProvider; use crate::{Client, JsonRpcRequest, JsonRpcResponse, error_codes}; +const MAX_BINARY_BYTES: usize = (64 * 1024 * 1024 - 1024) / 4 * 3; + /// Helper: serialize a typed result, send the response. async fn respond(client: &Client, request_id: u64, result: T) { let value = match serde_json::to_value(&result) { @@ -94,6 +98,53 @@ pub(crate) async fn read_file( respond(client, id, result).await; } +pub(crate) async fn read_file_bytes( + client: &Client, + provider: &Arc, + request: JsonRpcRequest, +) { + let params: SessionFsReadFileBytesRequest = match parse_params(&request) { + Some(params) => params, + None => { + send_error(client, request.id, "invalid sessionFs.readFileBytes params").await; + return; + } + }; + let result = match provider.binary() { + Some(binary) => binary.read_file_bytes(¶ms.path).await, + None => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "binary reads are not supported", + )), + }; + respond(client, request.id, binary_read_response(result)).await; +} + +fn binary_read_response( + result: Result, crate::session_fs::FsError>, +) -> SessionFsReadFileBytesResult { + match result { + Ok(bytes) if bytes.len() > MAX_BINARY_BYTES => SessionFsReadFileBytesResult { + content: String::new(), + error: Some( + crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "sessionFs.readFileBytes content exceeds the binary read limit", + ) + .into_wire(), + ), + }, + Ok(bytes) => SessionFsReadFileBytesResult { + content: base64::engine::general_purpose::STANDARD.encode(bytes), + error: None, + }, + Err(error) => SessionFsReadFileBytesResult { + content: String::new(), + error: Some(error.into_wire()), + }, + } +} + pub(crate) async fn write_file( client: &Client, provider: &Arc, @@ -112,7 +163,54 @@ pub(crate) async fn write_file( .await { Ok(()) => respond(client, id, Value::Null).await, - Err(e) => respond(client, id, e.into_wire()).await, + Err(e) => respond(client, id, e.into_write_wire()).await, + } +} + +pub(crate) async fn write_file_bytes( + client: &Client, + provider: &Arc, + request: JsonRpcRequest, +) { + let params: SessionFsWriteFileBytesRequest = match parse_params(&request) { + Some(params) => params, + None => { + send_error( + client, + request.id, + "invalid sessionFs.writeFileBytes params", + ) + .await; + return; + } + }; + let result = if params.content.len() > MAX_BINARY_BYTES.div_ceil(3) * 4 { + Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "sessionFs.writeFileBytes content exceeds the binary write limit", + )) + } else { + match base64::engine::general_purpose::STANDARD.decode(¶ms.content) { + Ok(content) => match provider.binary() { + Some(binary) => { + binary + .write_file_bytes(¶ms.path, &content, params.mode) + .await + } + None => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + "binary writes are not supported", + )), + }, + Err(error) => Err(crate::session_fs::FsError::with_message( + crate::session_fs::FsErrorKind::Other, + format!("invalid sessionFs.writeFileBytes base64 content: {error}"), + )), + } + }; + match result { + Ok(()) => respond(client, request.id, Value::Null).await, + Err(error) => respond(client, request.id, error.into_wire()).await, } } @@ -334,6 +432,7 @@ pub(crate) async fn sqlite_query( message: Some( "SQLite is not supported by this SessionFs provider".to_string(), ), + write_changed: None, }), last_insert_rowid: None, rows: Vec::new(), @@ -487,6 +586,8 @@ pub(crate) async fn dispatch( }; match method { "sessionFs.readFile" => read_file(client, &provider, request).await, + "sessionFs.readFileBytes" => read_file_bytes(client, &provider, request).await, + "sessionFs.writeFileBytes" => write_file_bytes(client, &provider, request).await, "sessionFs.writeFile" => write_file(client, &provider, request).await, "sessionFs.appendFile" => append_file(client, &provider, request).await, "sessionFs.exists" => exists(client, &provider, request).await, @@ -506,3 +607,6 @@ pub(crate) async fn dispatch( } true } + +#[cfg(test)] +mod tests; diff --git a/rust/src/session_fs_dispatch/tests.rs b/rust/src/session_fs_dispatch/tests.rs new file mode 100644 index 0000000000..c0f70d78b9 --- /dev/null +++ b/rust/src/session_fs_dispatch/tests.rs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![cfg(test)] + +use super::*; + +#[test] +fn oversized_binary_provider_result_is_a_filesystem_error() { + let result = binary_read_response(Ok(vec![0; MAX_BINARY_BYTES + 1])); + assert!(result.content.is_empty()); + let error = result.error.expect("oversized result must carry an error"); + assert_eq!(error.code, SessionFsErrorCode::UNKNOWN); + assert!( + error + .message + .as_deref() + .is_some_and(|message| message.contains("binary read limit")) + ); +} + +#[test] +fn small_binary_provider_result_preserves_exact_bytes() { + let result = binary_read_response(Ok(vec![0, 255, 254, 1])); + assert_eq!(result.content, "AP/+AQ=="); + assert!(result.error.is_none()); +} diff --git a/rust/src/skill_provider.rs b/rust/src/skill_provider.rs new file mode 100644 index 0000000000..28d4fc8911 --- /dev/null +++ b/rust/src/skill_provider.rs @@ -0,0 +1,40 @@ +//! Session-scoped skill provider callbacks. +//! +//!
+//! +//! **Experimental.** These types are part of an experimental wire-protocol +//! surface and may change or be removed in future SDK or CLI releases. +//! +//!
+ +use async_trait::async_trait; + +use crate::Error; +pub use crate::rpc::SkillProviderDescriptor; + +/// Supplies session-scoped skills to the runtime. +/// +///
+/// +/// **Experimental.** This trait is part of an experimental wire-protocol +/// surface and may change or be removed in future SDK or CLI releases. +/// +///
+/// +/// Each inbound `skillProvider.*` request is dispatched on its own spawned +/// task, so implementations must be safe for concurrent calls. When the +/// runtime cancels a call, for example after its time limit or when the +/// session disconnects, the SDK drops the provider future and the runtime +/// ignores any later result. Register a +/// provider with [`SessionConfig::with_skill_provider`](crate::SessionConfig::with_skill_provider) +/// or [`ResumeSessionConfig::with_skill_provider`](crate::ResumeSessionConfig::with_skill_provider). +#[async_trait] +pub trait SkillProvider: Send + Sync + 'static { + /// Return the catalog of skills this session exposes. + async fn list_skills(&self) -> std::result::Result, Error>; + + /// Return the markdown for one skill name. + /// + /// Return `Ok(None)` when the skill name is not found. + async fn read_skill(&self, name: &str) -> std::result::Result, Error>; +} diff --git a/rust/src/startup_timings.rs b/rust/src/startup_timings.rs index 4202b1e465..bf0166772d 100644 --- a/rust/src/startup_timings.rs +++ b/rust/src/startup_timings.rs @@ -81,27 +81,4 @@ impl StartupTimings { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn millis_truncates_to_whole_milliseconds() { - assert_eq!(StartupTimings::millis(Duration::from_micros(1_999)), 1); - assert_eq!(StartupTimings::millis(Duration::from_millis(250)), 250); - assert_eq!(StartupTimings::millis(Duration::ZERO), 0); - } - - #[test] - fn default_leaves_every_phase_unset() { - let timings = StartupTimings::default(); - assert_eq!(timings, StartupTimings::default()); - assert!(timings.program_resolve_ms.is_none()); - assert!(timings.process_spawn_ms.is_none()); - assert!(timings.port_wait_ms.is_none()); - assert_eq!(timings.transport_setup_ms, 0); - assert_eq!(timings.handshake_ms, 0); - assert!(timings.session_fs_ms.is_none()); - assert!(timings.llm_handler_ms.is_none()); - assert_eq!(timings.total_ms, 0); - } -} +mod tests; diff --git a/rust/src/startup_timings/tests.rs b/rust/src/startup_timings/tests.rs new file mode 100644 index 0000000000..f59f60ac04 --- /dev/null +++ b/rust/src/startup_timings/tests.rs @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn millis_truncates_to_whole_milliseconds() { + assert_eq!(StartupTimings::millis(Duration::from_micros(1_999)), 1); + assert_eq!(StartupTimings::millis(Duration::from_millis(250)), 250); + assert_eq!(StartupTimings::millis(Duration::ZERO), 0); +} + +#[test] +fn default_leaves_every_phase_unset() { + let timings = StartupTimings::default(); + assert_eq!(timings, StartupTimings::default()); + assert!(timings.program_resolve_ms.is_none()); + assert!(timings.process_spawn_ms.is_none()); + assert!(timings.port_wait_ms.is_none()); + assert_eq!(timings.transport_setup_ms, 0); + assert_eq!(timings.handshake_ms, 0); + assert!(timings.session_fs_ms.is_none()); + assert!(timings.llm_handler_ms.is_none()); + assert_eq!(timings.total_ms, 0); +} diff --git a/rust/src/tests.rs b/rust/src/tests.rs new file mode 100644 index 0000000000..8656d6f4d3 --- /dev/null +++ b/rust/src/tests.rs @@ -0,0 +1,813 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +#[test] +fn is_transport_failure_matches_request_cancelled() { + let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled)); + assert!(err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_matches_io_error() { + let err = Error::from(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "gone")); + assert!(err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_rejects_rpc_error() { + let err = Error::with_message(ErrorKind::Rpc { code: -1 }, "bad"); + assert!(!err.is_transport_failure()); +} + +#[test] +fn is_transport_failure_rejects_session_error() { + let err = Error::from(ErrorKind::Session(SessionErrorKind::NotFound("s1".into()))); + assert!(!err.is_transport_failure()); +} + +#[test] +fn client_options_builder_composes() { + let opts = ClientOptions::new() + .with_program(CliProgram::Path(PathBuf::from("/usr/local/bin/copilot"))) + .with_prefix_args(["node"]) + .with_cwd(PathBuf::from("/tmp")) + .with_env([("KEY", "value")]) + .with_env_remove(["UNWANTED"]) + .with_extra_args(["--quiet"]) + .with_github_token("ghp_test") + .with_use_logged_in_user(false) + .with_log_level(LogLevel::Debug) + .with_session_idle_timeout_seconds(120) + .with_enable_remote_sessions(true); + assert!(matches!(opts.program, CliProgram::Path(_))); + assert_eq!(opts.prefix_args, vec![std::ffi::OsString::from("node")]); + assert_eq!(opts.working_directory, PathBuf::from("/tmp")); + assert_eq!( + opts.env, + vec![( + std::ffi::OsString::from("KEY"), + std::ffi::OsString::from("value") + )] + ); + assert_eq!(opts.env_remove, vec![std::ffi::OsString::from("UNWANTED")]); + assert_eq!(opts.extra_args, vec!["--quiet".to_string()]); + assert_eq!(opts.github_token.as_deref(), Some("ghp_test")); + assert_eq!(opts.use_logged_in_user, Some(false)); + assert!(matches!(opts.log_level, Some(LogLevel::Debug))); + assert_eq!(opts.session_idle_timeout_seconds, Some(120)); + assert!(opts.enable_remote_sessions); +} + +#[test] +fn default_transport_values_resolve_without_process_state() { + assert!(matches!( + resolve_default_transport_value(None).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("stdio")).unwrap(), + Transport::Stdio + )); + assert!(matches!( + resolve_default_transport_value(Some("INPROCESS")).unwrap(), + Transport::InProcess + )); + assert!(resolve_default_transport_value(Some("tcp")).is_err()); +} + +#[test] +fn inprocess_rejects_process_scoped_options() { + let invalid = [ + ClientOptions::new().with_cwd("."), + ClientOptions::new().with_env([("KEY", "value")]), + ClientOptions::new().with_env_remove(["KEY"]), + ClientOptions::new().with_telemetry(TelemetryConfig::default()), + ClientOptions::new().with_prefix_args(["index.js"]), + ClientOptions::new().with_program(CliProgram::Path("copilot".into())), + ClientOptions::new().with_extra_args(["--verbose"]), + ]; + + for options in invalid { + assert!(validate_inprocess_options(&options).is_err()); + } +} + +#[test] +fn inprocess_allows_typed_runtime_options() { + let options = ClientOptions::new() + .with_base_directory("state") + .with_log_level(LogLevel::Debug) + .with_session_idle_timeout_seconds(10) + .with_github_token("token") + .with_use_logged_in_user(false) + .with_enable_remote_sessions(true); + + assert!(validate_inprocess_options(&options).is_ok()); +} + +#[cfg(not(feature = "in-process"))] +#[tokio::test] +async fn inprocess_requires_cargo_feature() { + let error = Client::start(ClientOptions::new().with_transport(Transport::InProcess)) + .await + .unwrap_err(); + + assert!(error.to_string().contains("in-process")); +} + +#[test] +fn is_transport_failure_rejects_other_protocol_errors() { + let err = Error::from(ErrorKind::Protocol(ProtocolErrorKind::CliStartupTimeout)); + assert!(!err.is_transport_failure()); +} + +#[test] +fn build_command_lets_env_remove_strip_injected_token() { + let opts = ClientOptions { + github_token: Some("secret".to_string()), + env_remove: vec![std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN")], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + // get_envs() iter yields the latest action per key — None means removed. + let action = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .map(|(_, v)| v); + assert_eq!( + action, + Some(None), + "env_remove should win over github_token" + ); +} + +#[test] +fn build_command_lets_env_override_injected_token() { + let opts = ClientOptions { + github_token: Some("from-options".to_string()), + env: vec![( + std::ffi::OsString::from("COPILOT_SDK_AUTH_TOKEN"), + std::ffi::OsString::from("from-env"), + )], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + let value = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .and_then(|(_, v)| v); + assert_eq!(value, Some(std::ffi::OsStr::new("from-env"))); +} + +#[test] +fn build_command_injects_github_token_by_default() { + let opts = ClientOptions { + github_token: Some("just-the-token".to_string()), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + let value = cmd + .as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new("COPILOT_SDK_AUTH_TOKEN")) + .and_then(|(_, v)| v); + assert_eq!(value, Some(std::ffi::OsStr::new("just-the-token"))); +} + +fn env_value<'a>(cmd: &'a tokio::process::Command, key: &str) -> Option<&'a std::ffi::OsStr> { + cmd.as_std() + .get_envs() + .find(|(k, _)| *k == std::ffi::OsStr::new(key)) + .and_then(|(_, v)| v) +} + +#[test] +fn telemetry_config_builder_composes() { + let cfg = TelemetryConfig::new() + .with_otlp_endpoint("http://collector:4318") + .with_otlp_protocol(OtlpHttpProtocol::HttpProtobuf) + .with_file_path(PathBuf::from("/var/log/copilot.jsonl")) + .with_exporter_type(OtelExporterType::OtlpHttp) + .with_source_name("my-app") + .with_capture_content(true); + + assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://collector:4318")); + assert_eq!(cfg.otlp_protocol, Some(OtlpHttpProtocol::HttpProtobuf)); + assert_eq!( + cfg.file_path.as_deref(), + Some(Path::new("/var/log/copilot.jsonl")), + ); + assert_eq!(cfg.exporter_type, Some(OtelExporterType::OtlpHttp)); + assert_eq!(cfg.source_name.as_deref(), Some("my-app")); + assert_eq!(cfg.capture_content, Some(true)); + assert!(!cfg.is_empty()); + assert!(TelemetryConfig::new().is_empty()); +} + +#[test] +fn otlp_http_protocol_serde_matches_env_value() { + for (protocol, wire) in [ + (OtlpHttpProtocol::HttpJson, "http/json"), + (OtlpHttpProtocol::HttpProtobuf, "http/protobuf"), + ] { + assert_eq!(protocol.as_str(), wire); + + let serialized = serde_json::to_string(&protocol).unwrap(); + assert_eq!(serialized, format!("\"{wire}\"")); + + let deserialized: OtlpHttpProtocol = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, protocol); + } +} + +#[test] +fn build_command_sets_otel_env_when_telemetry_enabled() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://collector:4318".to_string()), + otlp_protocol: Some(OtlpHttpProtocol::HttpProtobuf), + file_path: Some(PathBuf::from("/var/log/copilot.jsonl")), + exporter_type: Some(OtelExporterType::OtlpHttp), + source_name: Some("my-app".to_string()), + capture_content: Some(true), + }), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_ENABLED"), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://collector:4318")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_PROTOCOL"), + Some(std::ffi::OsStr::new("http/protobuf")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_FILE_EXPORTER_PATH"), + Some(std::ffi::OsStr::new("/var/log/copilot.jsonl")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_EXPORTER_TYPE"), + Some(std::ffi::OsStr::new("otlp-http")), + ); + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_SOURCE_NAME"), + Some(std::ffi::OsStr::new("my-app")), + ); + assert_eq!( + env_value(&cmd, "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"), + Some(std::ffi::OsStr::new("true")), + ); +} + +#[test] +fn build_command_omits_otel_env_when_telemetry_none() { + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + for key in [ + "COPILOT_OTEL_ENABLED", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_PROTOCOL", + "COPILOT_OTEL_FILE_EXPORTER_PATH", + "COPILOT_OTEL_EXPORTER_TYPE", + "COPILOT_OTEL_SOURCE_NAME", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + ] { + assert!( + env_value(&cmd, key).is_none(), + "expected {key} to be unset when telemetry is None", + ); + } +} + +#[test] +fn build_command_omits_unset_telemetry_fields() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://collector:4318".to_string()), + ..Default::default() + }), + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + // The one set field plus the implicit enabled flag should propagate. + assert_eq!( + env_value(&cmd, "COPILOT_OTEL_ENABLED"), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://collector:4318")), + ); + // None of the other fields should leak as env vars. + for key in [ + "OTEL_EXPORTER_OTLP_PROTOCOL", + "COPILOT_OTEL_FILE_EXPORTER_PATH", + "COPILOT_OTEL_EXPORTER_TYPE", + "COPILOT_OTEL_SOURCE_NAME", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + ] { + assert!(env_value(&cmd, key).is_none(), "{key} should be unset"); + } +} + +#[test] +fn build_command_lets_user_env_override_telemetry() { + let opts = ClientOptions { + telemetry: Some(TelemetryConfig { + otlp_endpoint: Some("http://from-config:4318".to_string()), + ..Default::default() + }), + env: vec![( + std::ffi::OsString::from("OTEL_EXPORTER_OTLP_ENDPOINT"), + std::ffi::OsString::from("http://from-user-env:4318"), + )], + ..Default::default() + }; + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"), + Some(std::ffi::OsStr::new("http://from-user-env:4318")), + "user-supplied options.env should override telemetry config", + ); +} + +#[test] +fn build_command_sets_copilot_home_env_when_configured() { + let opts = ClientOptions::new().with_base_directory(PathBuf::from("/custom/copilot")); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_HOME"), + Some(std::ffi::OsStr::new("/custom/copilot")), + ); + + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert!(env_value(&cmd, "COPILOT_HOME").is_none()); +} + +#[test] +fn build_command_sets_connection_token_env_when_configured() { + let opts = ClientOptions::new().with_transport(Transport::Tcp { + port: 0, + connection_token: Some("secret-token".to_string()), + }); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert_eq!( + env_value(&cmd, "COPILOT_CONNECTION_TOKEN"), + Some(std::ffi::OsStr::new("secret-token")), + ); + + let opts = ClientOptions::default(); + let cmd = Client::build_command(Path::new("/bin/echo"), &opts, Path::new("/tmp")); + assert!(env_value(&cmd, "COPILOT_CONNECTION_TOKEN").is_none()); +} + +#[tokio::test] +async fn start_rejects_empty_connection_token() { + let opts = ClientOptions::new() + .with_transport(Transport::Tcp { + port: 0, + connection_token: Some(String::new()), + }) + .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); + let err = Client::start(opts).await.unwrap_err(); + assert!( + matches!(err.kind(), ErrorKind::InvalidConfig), + "got {err:?}" + ); +} + +#[tokio::test] +async fn start_rejects_empty_external_connection_token() { + let opts = ClientOptions::new() + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port: 1, + connection_token: Some(String::new()), + }) + .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); + let err = Client::start(opts).await.unwrap_err(); + assert!( + matches!(err.kind(), ErrorKind::InvalidConfig), + "got {err:?}" + ); +} + +#[test] +fn telemetry_config_capture_content_serializes_as_lowercase_bool() { + let opts_true = ClientOptions { + telemetry: Some(TelemetryConfig { + capture_content: Some(true), + ..Default::default() + }), + ..Default::default() + }; + let opts_false = ClientOptions { + telemetry: Some(TelemetryConfig { + capture_content: Some(false), + ..Default::default() + }), + ..Default::default() + }; + let cmd_true = Client::build_command(Path::new("/bin/echo"), &opts_true, Path::new("/tmp")); + let cmd_false = Client::build_command(Path::new("/bin/echo"), &opts_false, Path::new("/tmp")); + assert_eq!( + env_value( + &cmd_true, + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" + ), + Some(std::ffi::OsStr::new("true")), + ); + assert_eq!( + env_value( + &cmd_false, + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" + ), + Some(std::ffi::OsStr::new("false")), + ); +} + +#[test] +fn session_idle_timeout_args_are_omitted_by_default() { + let opts = ClientOptions::default(); + assert!(Client::session_idle_timeout_args(&opts).is_empty()); +} + +#[test] +fn session_idle_timeout_args_omitted_for_zero() { + let opts = ClientOptions { + session_idle_timeout_seconds: Some(0), + ..Default::default() + }; + assert!(Client::session_idle_timeout_args(&opts).is_empty()); +} + +#[test] +fn session_idle_timeout_args_emit_flag_for_positive_value() { + let opts = ClientOptions { + session_idle_timeout_seconds: Some(300), + ..Default::default() + }; + assert_eq!( + Client::session_idle_timeout_args(&opts), + vec!["--session-idle-timeout".to_string(), "300".to_string()] + ); +} + +#[test] +fn remote_args_omitted_by_default() { + let opts = ClientOptions::default(); + assert!(Client::remote_args(&opts).is_empty()); +} + +#[test] +fn remote_args_emit_flag_when_enabled() { + let opts = ClientOptions { + enable_remote_sessions: true, + ..Default::default() + }; + assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); +} + +#[test] +fn log_level_args_omitted_when_unset() { + let opts = ClientOptions::default(); + assert!(opts.log_level.is_none()); + assert!( + Client::log_level_args(&opts).is_empty(), + "with no caller-supplied log_level the SDK must not pass --log-level" + ); +} + +#[test] +fn log_level_args_emit_flag_when_set() { + let opts = ClientOptions::default().with_log_level(LogLevel::Debug); + assert_eq!(Client::log_level_args(&opts), vec!["--log-level", "debug"]); +} + +#[test] +fn cli_mode_opts_into_process_logging_without_changing_empty_mode_environment() { + for (mode, expected) in [ + (ClientMode::Empty, None), + (ClientMode::CopilotCli, Some("1")), + ] { + let mut options = ClientOptions::default().with_mode(mode); + options.env.push(( + std::ffi::OsString::from("COPILOT_RUNTIME_PROCESS_FILE_LOGGING"), + std::ffi::OsString::from("opposite"), + )); + let command = Client::build_command(Path::new("copilot-runtime"), &options, Path::new(".")); + let actual = command + .as_std() + .get_envs() + .find(|(key, _)| *key == std::ffi::OsStr::new("COPILOT_RUNTIME_PROCESS_FILE_LOGGING")) + .and_then(|(_, value)| value); + assert_eq!( + actual, + Some(std::ffi::OsStr::new(expected.unwrap_or("opposite"))), + "mode: {mode:?}" + ); + } +} + +#[test] +fn log_level_str_round_trips() { + for level in [ + LogLevel::None, + LogLevel::Error, + LogLevel::Warning, + LogLevel::Info, + LogLevel::Debug, + LogLevel::All, + ] { + let s = level.as_str(); + let json = serde_json::to_string(&level).unwrap(); + assert_eq!(json, format!("\"{s}\"")); + let parsed: LogLevel = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, level); + } +} + +#[test] +fn client_options_debug_redacts_handler() { + struct StubHandler; + #[async_trait] + impl ListModelsHandler for StubHandler { + async fn list_models(&self) -> Result> { + Ok(vec![]) + } + } + let opts = ClientOptions { + on_list_models: Some(Arc::new(StubHandler)), + github_token: Some("secret-token".into()), + ..Default::default() + }; + let debug = format!("{opts:?}"); + assert!(debug.contains("on_list_models: Some(\"\")")); + assert!(debug.contains("github_token: Some(\"\")")); + assert!(!debug.contains("secret-token")); +} + +#[tokio::test] +async fn list_models_uses_on_list_models_handler_when_set() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct CountingHandler { + calls: Arc, + models: Vec, + } + #[async_trait] + impl ListModelsHandler for CountingHandler { + async fn list_models(&self) -> Result> { + self.calls.fetch_add(1, Ordering::SeqCst); + Ok(self.models.clone()) + } + } + + let calls = Arc::new(AtomicUsize::new(0)); + let model = Model { + id: "byok-gpt-4".into(), + name: "BYOK GPT-4".into(), + ..Default::default() + }; + let handler: Arc = Arc::new(CountingHandler { + calls: Arc::clone(&calls), + models: vec![model.clone()], + }); + + let client = client_with_list_models_handler(handler); + + let result = client.list_models().await.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, "byok-gpt-4"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn list_models_serializes_concurrent_cache_misses() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct SlowCountingHandler { + calls: Arc, + models: Vec, + } + #[async_trait] + impl ListModelsHandler for SlowCountingHandler { + async fn list_models(&self) -> Result> { + self.calls.fetch_add(1, Ordering::SeqCst); + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + Ok(self.models.clone()) + } + } + + let calls = Arc::new(AtomicUsize::new(0)); + let model = Model { + id: "single-flight-model".into(), + name: "Single Flight Model".into(), + ..Default::default() + }; + let handler: Arc = Arc::new(SlowCountingHandler { + calls: Arc::clone(&calls), + models: vec![model], + }); + let client = client_with_list_models_handler(handler); + + let (first, second) = tokio::join!(client.list_models(), client.list_models()); + assert_eq!(first.unwrap()[0].id, "single-flight-model"); + assert_eq!(second.unwrap()[0].id, "single-flight-model"); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn cancelled_resume_session_unregisters_pending_session() { + let (client_write, _server_read) = tokio::io::duplex(8192); + let (_server_write, client_read) = tokio::io::duplex(8192); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + assert!(client.startup_timings().is_none()); + let session_id = SessionId::new("resume-cancel-test"); + let handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + } + }); + + wait_for_pending_session_registration(&client).await; + handle.abort(); + let _ = handle.await; + + assert!(client.inner.router.session_ids().is_empty()); + client.force_stop(); +} + +#[cfg(any(unix, windows))] +#[tokio::test] +async fn dropping_last_client_kills_spawned_cli() { + let temp = tempfile::tempdir().unwrap(); + let ready = temp.path().join("ready"); + let survived = temp.path().join("survived"); + let child = test_child_command(temp.path(), &ready, &survived) + .spawn() + .unwrap(); + let (client_write, _server_read) = tokio::io::duplex(64); + let (_server_write, client_read) = tokio::io::duplex(64); + let client = Client::from_transport( + client_read, + client_write, + Some(child), + None, + temp.path().to_path_buf(), + None, + None, + false, + false, + false, + None, + None, + None, + ClientMode::default(), + None, + false, + ) + .unwrap(); + + wait_for_test_child(&ready).await; + drop(client); + + assert_test_child_killed(&survived).await; +} + +#[cfg(any(unix, windows))] +#[tokio::test] +async fn spawned_child_is_killed_when_dropped() { + let temp = tempfile::tempdir().unwrap(); + let ready = temp.path().join("ready"); + let survived = temp.path().join("survived"); + let child = test_child_command(temp.path(), &ready, &survived) + .spawn() + .unwrap(); + + wait_for_test_child(&ready).await; + drop(child); + + assert_test_child_killed(&survived).await; +} + +#[cfg(any(unix, windows))] +fn test_child_command(temp: &Path, ready: &Path, survived: &Path) -> Command { + let mut command = Client::build_command(Path::new("node"), &ClientOptions::default(), temp); + #[cfg(windows)] + { + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + command + .args([ + "-e", + r#" + const fs = require("node:fs"); + fs.writeFileSync(process.env.READY, "ready"); + setTimeout(() => fs.writeFileSync(process.env.SURVIVED, "survived"), 1000); + "#, + ]) + .env("READY", ready) + .env("SURVIVED", survived) + .stderr(Stdio::inherit()); + command +} + +#[cfg(any(unix, windows))] +async fn wait_for_test_child(ready: &Path) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(30); + while !ready.exists() { + assert!( + tokio::time::Instant::now() < deadline, + "child did not report readiness" + ); + tokio::time::sleep(Duration::from_millis(10)).await; + } +} + +#[cfg(any(unix, windows))] +async fn assert_test_child_killed(survived: &Path) { + tokio::time::sleep(Duration::from_millis(1500)).await; + + assert!( + !survived.exists(), + "child survived after its owner was dropped" + ); +} + +fn client_with_list_models_handler(handler: Arc) -> Client { + Client { + ahp_host_sessions: None, + inner: Arc::new(ClientInner { + ahp_host_callbacks: Arc::new(ahp_host::ExitCallbacks::default()), + ahp_host_sessions: std::sync::Weak::new(), + child: parking_lot::Mutex::new(None), + owns_stdio: false, + force_stop_requested: tokio_util::sync::CancellationToken::new(), + process_tree: parking_lot::Mutex::new(None), + #[cfg(feature = "in-process")] + ffi_host: parking_lot::Mutex::new(None), + rpc: { + let (req_tx, _req_rx) = mpsc::unbounded_channel(); + let (notif_tx, _notif_rx) = broadcast::channel(16); + let (read_pipe, _write_pipe) = tokio::io::duplex(64); + let (_unused_read, write_pipe) = tokio::io::duplex(64); + JsonRpcClient::new(write_pipe, read_pipe, notif_tx, req_tx) + }, + cwd: PathBuf::from("."), + request_rx: parking_lot::Mutex::new(None), + notification_tx: broadcast::channel(16).0, + router: router::SessionRouter::new(), + github_token_registry: Arc::new(github_token::GitHubTokenRegistry::new()), + negotiated_protocol_version: OnceLock::new(), + state: parking_lot::Mutex::new(ConnectionState::Connected), + lifecycle_tx: broadcast::channel(16).0, + on_list_models: Some(handler), + models_cache: parking_lot::Mutex::new(Arc::new(tokio::sync::OnceCell::new())), + session_fs_configured: false, + session_fs_sqlite_declared: false, + session_fs_binary_declared: false, + llm_inference: OnceLock::new(), + extension_launch_provider: Arc::new( + extension_launch_provider::ExtensionLaunchProviderDispatcher::new(None), + ), + installation_confirmation: Arc::new( + installation_confirmation::InstallationConfirmationDispatcher::new(), + ), + on_github_telemetry: None, + on_get_trace_context: None, + effective_connection_token: None, + mode: ClientMode::default(), + client_info: None, + startup_timings: OnceLock::new(), + }), + } +} + +async fn wait_for_pending_session_registration(client: &Client) { + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1); + while client.inner.router.session_ids().is_empty() { + assert!( + tokio::time::Instant::now() < deadline, + "session was not registered" + ); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } +} diff --git a/rust/src/tool.rs b/rust/src/tool.rs index 344d2894ca..12274dbb8b 100644 --- a/rust/src/tool.rs +++ b/rust/src/tool.rs @@ -363,423 +363,4 @@ where } #[cfg(test)] -mod tests { - use super::*; - use crate::types::SessionId; - - struct EchoTool; - - fn echo_tool() -> Tool { - Tool { - name: "echo".to_string(), - description: "Echo the input".to_string(), - parameters: tool_parameters(serde_json::json!({"type": "object"})), - ..Default::default() - } - .with_handler(std::sync::Arc::new(EchoTool)) - } - - #[async_trait] - impl ToolHandler for EchoTool { - async fn call(&self, inv: ToolInvocation) -> Result { - Ok(ToolResult::Text(inv.arguments.to_string())) - } - } - - #[test] - fn tool_handler_returns_tool_definition() { - let def = echo_tool(); - assert_eq!(def.name, "echo"); - assert_eq!(def.description, "Echo the input"); - assert!(def.parameters.contains_key("type")); - assert!(def.handler.is_some()); - } - - #[test] - fn try_tool_parameters_rejects_non_object_schema() { - let err = try_tool_parameters(serde_json::json!(["not", "an", "object"])) - .expect_err("non-object schemas should be rejected"); - - assert!(err.is_data()); - } - - #[test] - fn tool_parameters_serialize_in_deterministic_order() { - // Regression: `Tool.parameters` was a `HashMap`, whose per-instance - // random iteration order made the serialized top-level schema keys - // differ between constructions (and between sessions), busting the - // model provider's prompt cache. `IndexMap` keeps the order stable. - let schema = serde_json::json!({ - "type": "object", - "properties": { - "url": { "type": "string" }, - "count": { "type": "integer" } - }, - "required": ["url"], - "additionalProperties": false - }); - - let build = || Tool { - name: "fetch".to_string(), - parameters: tool_parameters(schema.clone()), - ..Default::default() - }; - - let expected = serde_json::to_string(&build()).expect("serialize tool"); - for _ in 0..64 { - let actual = serde_json::to_string(&build()).expect("serialize tool"); - assert_eq!(actual, expected); - } - - // Pin the exact top-level key order so a regression to any - // order-randomizing container is caught, not just internal drift. - let tool = build(); - let keys: Vec<&str> = tool.parameters.keys().map(String::as_str).collect(); - assert_eq!( - keys, - ["additionalProperties", "properties", "required", "type"] - ); - } - - #[test] - fn convert_mcp_call_tool_result_collects_text_and_binary_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "isError": true, - "content": [ - { "type": "text", "text": "hello" }, - { "type": "image", "data": "aW1n", "mimeType": "image/png" }, - { - "type": "resource", - "resource": { - "uri": "file:///tmp/data.bin", - "blob": "Ymlu", - "mimeType": "application/octet-stream", - "text": "resource text" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); - assert_eq!(expanded.result_type, "failure"); - let binary_results = expanded - .binary_results_for_llm - .expect("binary results should be captured"); - assert_eq!(binary_results.len(), 2); - assert_eq!(binary_results[0].r#type, "image"); - assert_eq!(binary_results[0].data, "aW1n"); - assert_eq!(binary_results[0].mime_type, "image/png"); - assert_eq!( - binary_results[1].description.as_deref(), - Some("file:///tmp/data.bin") - ); - } - - #[test] - fn convert_mcp_call_tool_result_converts_image_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/jpeg" } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, ""); - assert_eq!(expanded.result_type, "success"); - let binary_results = expanded - .binary_results_for_llm - .expect("image result should be captured"); - assert_eq!(binary_results.len(), 1); - assert_eq!(binary_results[0].data, "aW1hZ2U="); - assert_eq!(binary_results[0].mime_type, "image/jpeg"); - assert_eq!(binary_results[0].r#type, "image"); - assert!(binary_results[0].description.is_none()); - } - - #[test] - fn convert_mcp_call_tool_result_converts_resource_blob_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { - "type": "resource", - "resource": { - "uri": "file:///tmp/report.pdf", - "blob": "cGRm", - "mimeType": "application/pdf" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - let binary_results = expanded - .binary_results_for_llm - .expect("resource result should be captured"); - assert_eq!(binary_results.len(), 1); - assert_eq!(binary_results[0].data, "cGRm"); - assert_eq!(binary_results[0].mime_type, "application/pdf"); - assert_eq!(binary_results[0].r#type, "resource"); - assert_eq!( - binary_results[0].description.as_deref(), - Some("file:///tmp/report.pdf") - ); - } - - #[test] - fn convert_mcp_call_tool_result_defaults_resource_blob_mime_type() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { - "type": "resource", - "resource": { - "uri": "file:///tmp/data.bin", - "blob": "Ymlu" - } - }, - { - "type": "resource", - "resource": { - "blob": "YmluMg==", - "mimeType": "" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - let binary_results = expanded - .binary_results_for_llm - .expect("resource blobs should be captured"); - assert_eq!(binary_results.len(), 2); - assert_eq!(binary_results[0].mime_type, "application/octet-stream"); - assert_eq!(binary_results[1].mime_type, "application/octet-stream"); - } - - #[test] - fn convert_mcp_call_tool_result_omits_binary_results_without_binary_content() { - let result = convert_mcp_call_tool_result(&serde_json::json!({ - "content": [ - { "type": "text", "text": "hello" }, - { - "type": "resource", - "resource": { - "uri": "file:///tmp/readme.md", - "text": "resource text" - } - } - ] - })) - .expect("valid CallToolResult should convert"); - - let ToolResult::Expanded(expanded) = result else { - panic!("expected expanded tool result"); - }; - - assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); - assert!(expanded.binary_results_for_llm.is_none()); - } - - #[tokio::test] - async fn tool_handler_call_returns_result() { - let tool = EchoTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "echo".to_string(), - arguments: serde_json::json!({"msg": "hello"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let result = tool.call(inv).await.unwrap(); - match result { - ToolResult::Text(s) => assert!(s.contains("hello")), - _ => panic!("expected Text result"), - } - } - - #[cfg(feature = "derive")] - #[tokio::test] - async fn define_tool_builds_schema_and_dispatches() { - use serde::Deserialize; - - #[derive(Deserialize, schemars::JsonSchema)] - struct Params { - city: String, - } - - let tool = define_tool( - "weather", - "Get the weather for a city", - |_inv, params: Params| async move { - Ok(ToolResult::Text(format!("sunny in {}", params.city))) - }, - ); - - assert_eq!(tool.name, "weather"); - assert_eq!(tool.description, "Get the weather for a city"); - assert_eq!(tool.parameters["type"], "object"); - assert!(tool.parameters["properties"]["city"].is_object()); - let handler = tool.handler.as_ref().expect("define_tool attaches handler"); - - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "weather".to_string(), - arguments: serde_json::json!({"city": "Seattle"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - match handler.call(inv).await.unwrap() { - ToolResult::Text(s) => assert_eq!(s, "sunny in Seattle"), - _ => panic!("expected Text result"), - } - } - - // Tests requiring `schemars` (the `derive` feature). - #[cfg(feature = "derive")] - mod derive_tests { - use serde::Deserialize; - - use super::super::*; - use crate::{ErrorKind, SessionId}; - - #[derive(Deserialize, schemars::JsonSchema)] - struct GetWeatherParams { - /// City name to get weather for. - city: String, - /// Temperature unit (celsius or fahrenheit). - unit: Option, - } - - #[test] - fn schema_for_generates_clean_schema() { - let schema = schema_for::(); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["city"].is_object()); - assert!(schema["properties"]["unit"].is_object()); - // city is required (non-Option), unit is not - let required = schema["required"].as_array().unwrap(); - assert!(required.contains(&serde_json::json!("city"))); - assert!(!required.contains(&serde_json::json!("unit"))); - // Root-level metadata stripped - assert!(schema.get("$schema").is_none()); - assert!(schema.get("title").is_none()); - } - - struct GetWeatherTool; - - fn get_weather_tool() -> Tool { - Tool { - name: "get_weather".to_string(), - description: "Get weather for a city".to_string(), - parameters: tool_parameters(schema_for::()), - ..Default::default() - } - .with_handler(std::sync::Arc::new(GetWeatherTool)) - } - - #[async_trait] - impl ToolHandler for GetWeatherTool { - async fn call(&self, inv: ToolInvocation) -> Result { - let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; - Ok(ToolResult::Text(format!( - "{} {}", - params.city, - params.unit.unwrap_or_default() - ))) - } - } - - #[test] - fn tool_handler_with_schema_for() { - let def = get_weather_tool(); - assert_eq!(def.name, "get_weather"); - let schema = serde_json::to_value(&def.parameters).expect("serialize tool parameters"); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["city"].is_object()); - assert!(def.handler.is_some()); - } - - #[tokio::test] - async fn tool_handler_deserializes_typed_params() { - let tool = GetWeatherTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"city": "Seattle", "unit": "celsius"}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let result = tool.call(inv).await.unwrap(); - match result { - ToolResult::Text(s) => assert_eq!(s, "Seattle celsius"), - _ => panic!("expected Text result"), - } - } - - #[tokio::test] - async fn tool_handler_returns_error_on_bad_params() { - let tool = GetWeatherTool; - let inv = ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"wrong_field": 42}), - available_tools: None, - traceparent: None, - tracestate: None, - }; - - let err = tool.call(inv).await.unwrap_err(); - assert!(matches!(err.kind(), ErrorKind::Json)); - } - - #[tokio::test] - async fn schema_for_derived_tool_round_trips_through_call() { - let tool = GetWeatherTool; - - // Calling the tool with matching arguments returns the - // expected typed result. (Per-name dispatch is the SDK's - // concern; here we exercise just the handler contract.) - let result = tool - .call(ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "get_weather".to_string(), - arguments: serde_json::json!({"city": "Portland"}), - available_tools: None, - traceparent: None, - tracestate: None, - }) - .await - .expect("ToolHandler::call should succeed for matching args"); - match result { - ToolResult::Text(s) => assert!(s.contains("Portland")), - _ => panic!("expected ToolResult::Text"), - } - } - } -} +mod tests; diff --git a/rust/src/tool/tests.rs b/rust/src/tool/tests.rs new file mode 100644 index 0000000000..e9a0192964 --- /dev/null +++ b/rust/src/tool/tests.rs @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; +use crate::types::SessionId; + +struct EchoTool; + +fn echo_tool() -> Tool { + Tool { + name: "echo".to_string(), + description: "Echo the input".to_string(), + parameters: tool_parameters(serde_json::json!({"type": "object"})), + ..Default::default() + } + .with_handler(std::sync::Arc::new(EchoTool)) +} + +#[async_trait] +impl ToolHandler for EchoTool { + async fn call(&self, inv: ToolInvocation) -> Result { + Ok(ToolResult::Text(inv.arguments.to_string())) + } +} + +#[test] +fn tool_handler_returns_tool_definition() { + let def = echo_tool(); + assert_eq!(def.name, "echo"); + assert_eq!(def.description, "Echo the input"); + assert!(def.parameters.contains_key("type")); + assert!(def.handler.is_some()); +} + +#[test] +fn try_tool_parameters_rejects_non_object_schema() { + let err = try_tool_parameters(serde_json::json!(["not", "an", "object"])) + .expect_err("non-object schemas should be rejected"); + + assert!(err.is_data()); +} + +#[test] +fn tool_parameters_serialize_in_deterministic_order() { + // Regression: `Tool.parameters` was a `HashMap`, whose per-instance + // random iteration order made the serialized top-level schema keys + // differ between constructions (and between sessions), busting the + // model provider's prompt cache. `IndexMap` keeps the order stable. + let schema = serde_json::json!({ + "type": "object", + "properties": { + "url": { "type": "string" }, + "count": { "type": "integer" } + }, + "required": ["url"], + "additionalProperties": false + }); + + let build = || Tool { + name: "fetch".to_string(), + parameters: tool_parameters(schema.clone()), + ..Default::default() + }; + + let expected = serde_json::to_string(&build()).expect("serialize tool"); + for _ in 0..64 { + let actual = serde_json::to_string(&build()).expect("serialize tool"); + assert_eq!(actual, expected); + } + + // Pin the exact top-level key order so a regression to any + // order-randomizing container is caught, not just internal drift. + let tool = build(); + let keys: Vec<&str> = tool.parameters.keys().map(String::as_str).collect(); + assert_eq!( + keys, + ["additionalProperties", "properties", "required", "type"] + ); +} + +#[test] +fn convert_mcp_call_tool_result_collects_text_and_binary_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "isError": true, + "content": [ + { "type": "text", "text": "hello" }, + { "type": "image", "data": "aW1n", "mimeType": "image/png" }, + { + "type": "resource", + "resource": { + "uri": "file:///tmp/data.bin", + "blob": "Ymlu", + "mimeType": "application/octet-stream", + "text": "resource text" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); + assert_eq!(expanded.result_type, "failure"); + let binary_results = expanded + .binary_results_for_llm + .expect("binary results should be captured"); + assert_eq!(binary_results.len(), 2); + assert_eq!(binary_results[0].r#type, "image"); + assert_eq!(binary_results[0].data, "aW1n"); + assert_eq!(binary_results[0].mime_type, "image/png"); + assert_eq!( + binary_results[1].description.as_deref(), + Some("file:///tmp/data.bin") + ); +} + +#[test] +fn convert_mcp_call_tool_result_converts_image_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/jpeg" } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, ""); + assert_eq!(expanded.result_type, "success"); + let binary_results = expanded + .binary_results_for_llm + .expect("image result should be captured"); + assert_eq!(binary_results.len(), 1); + assert_eq!(binary_results[0].data, "aW1hZ2U="); + assert_eq!(binary_results[0].mime_type, "image/jpeg"); + assert_eq!(binary_results[0].r#type, "image"); + assert!(binary_results[0].description.is_none()); +} + +#[test] +fn convert_mcp_call_tool_result_converts_resource_blob_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { + "type": "resource", + "resource": { + "uri": "file:///tmp/report.pdf", + "blob": "cGRm", + "mimeType": "application/pdf" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + let binary_results = expanded + .binary_results_for_llm + .expect("resource result should be captured"); + assert_eq!(binary_results.len(), 1); + assert_eq!(binary_results[0].data, "cGRm"); + assert_eq!(binary_results[0].mime_type, "application/pdf"); + assert_eq!(binary_results[0].r#type, "resource"); + assert_eq!( + binary_results[0].description.as_deref(), + Some("file:///tmp/report.pdf") + ); +} + +#[test] +fn convert_mcp_call_tool_result_defaults_resource_blob_mime_type() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { + "type": "resource", + "resource": { + "uri": "file:///tmp/data.bin", + "blob": "Ymlu" + } + }, + { + "type": "resource", + "resource": { + "blob": "YmluMg==", + "mimeType": "" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + let binary_results = expanded + .binary_results_for_llm + .expect("resource blobs should be captured"); + assert_eq!(binary_results.len(), 2); + assert_eq!(binary_results[0].mime_type, "application/octet-stream"); + assert_eq!(binary_results[1].mime_type, "application/octet-stream"); +} + +#[test] +fn convert_mcp_call_tool_result_omits_binary_results_without_binary_content() { + let result = convert_mcp_call_tool_result(&serde_json::json!({ + "content": [ + { "type": "text", "text": "hello" }, + { + "type": "resource", + "resource": { + "uri": "file:///tmp/readme.md", + "text": "resource text" + } + } + ] + })) + .expect("valid CallToolResult should convert"); + + let ToolResult::Expanded(expanded) = result else { + panic!("expected expanded tool result"); + }; + + assert_eq!(expanded.text_result_for_llm, "hello\nresource text"); + assert!(expanded.binary_results_for_llm.is_none()); +} + +#[tokio::test] +async fn tool_handler_call_returns_result() { + let tool = EchoTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "echo".to_string(), + arguments: serde_json::json!({"msg": "hello"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let result = tool.call(inv).await.unwrap(); + match result { + ToolResult::Text(s) => assert!(s.contains("hello")), + _ => panic!("expected Text result"), + } +} + +#[cfg(feature = "derive")] +#[tokio::test] +async fn define_tool_builds_schema_and_dispatches() { + use serde::Deserialize; + + #[derive(Deserialize, schemars::JsonSchema)] + struct Params { + city: String, + } + + let tool = define_tool( + "weather", + "Get the weather for a city", + |_inv, params: Params| async move { Ok(ToolResult::Text(format!("sunny in {}", params.city))) }, + ); + + assert_eq!(tool.name, "weather"); + assert_eq!(tool.description, "Get the weather for a city"); + assert_eq!(tool.parameters["type"], "object"); + assert!(tool.parameters["properties"]["city"].is_object()); + let handler = tool.handler.as_ref().expect("define_tool attaches handler"); + + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "weather".to_string(), + arguments: serde_json::json!({"city": "Seattle"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + match handler.call(inv).await.unwrap() { + ToolResult::Text(s) => assert_eq!(s, "sunny in Seattle"), + _ => panic!("expected Text result"), + } +} + +// Tests requiring `schemars` (the `derive` feature). +#[cfg(feature = "derive")] +mod derive_tests { + use serde::Deserialize; + + use super::super::*; + use crate::{ErrorKind, SessionId}; + + #[derive(Deserialize, schemars::JsonSchema)] + struct GetWeatherParams { + /// City name to get weather for. + city: String, + /// Temperature unit (celsius or fahrenheit). + unit: Option, + } + + #[test] + fn schema_for_generates_clean_schema() { + let schema = schema_for::(); + assert_eq!(schema["type"], "object"); + assert!(schema["properties"]["city"].is_object()); + assert!(schema["properties"]["unit"].is_object()); + // city is required (non-Option), unit is not + let required = schema["required"].as_array().unwrap(); + assert!(required.contains(&serde_json::json!("city"))); + assert!(!required.contains(&serde_json::json!("unit"))); + // Root-level metadata stripped + assert!(schema.get("$schema").is_none()); + assert!(schema.get("title").is_none()); + } + + struct GetWeatherTool; + + fn get_weather_tool() -> Tool { + Tool { + name: "get_weather".to_string(), + description: "Get weather for a city".to_string(), + parameters: tool_parameters(schema_for::()), + ..Default::default() + } + .with_handler(std::sync::Arc::new(GetWeatherTool)) + } + + #[async_trait] + impl ToolHandler for GetWeatherTool { + async fn call(&self, inv: ToolInvocation) -> Result { + let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; + Ok(ToolResult::Text(format!( + "{} {}", + params.city, + params.unit.unwrap_or_default() + ))) + } + } + + #[test] + fn tool_handler_with_schema_for() { + let def = get_weather_tool(); + assert_eq!(def.name, "get_weather"); + let schema = serde_json::to_value(&def.parameters).expect("serialize tool parameters"); + assert_eq!(schema["type"], "object"); + assert!(schema["properties"]["city"].is_object()); + assert!(def.handler.is_some()); + } + + #[tokio::test] + async fn tool_handler_deserializes_typed_params() { + let tool = GetWeatherTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"city": "Seattle", "unit": "celsius"}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let result = tool.call(inv).await.unwrap(); + match result { + ToolResult::Text(s) => assert_eq!(s, "Seattle celsius"), + _ => panic!("expected Text result"), + } + } + + #[tokio::test] + async fn tool_handler_returns_error_on_bad_params() { + let tool = GetWeatherTool; + let inv = ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"wrong_field": 42}), + available_tools: None, + traceparent: None, + tracestate: None, + }; + + let err = tool.call(inv).await.unwrap_err(); + assert!(matches!(err.kind(), ErrorKind::Json)); + } + + #[tokio::test] + async fn schema_for_derived_tool_round_trips_through_call() { + let tool = GetWeatherTool; + + // Calling the tool with matching arguments returns the + // expected typed result. (Per-name dispatch is the SDK's + // concern; here we exercise just the handler contract.) + let result = tool + .call(ToolInvocation { + session_id: SessionId::from("s1"), + tool_call_id: "tc1".to_string(), + tool_name: "get_weather".to_string(), + arguments: serde_json::json!({"city": "Portland"}), + available_tools: None, + traceparent: None, + tracestate: None, + }) + .await + .expect("ToolHandler::call should succeed for matching args"); + match result { + ToolResult::Text(s) => assert!(s.contains("Portland")), + _ => panic!("expected ToolResult::Text"), + } + } +} diff --git a/rust/src/trace_context.rs b/rust/src/trace_context.rs index 287c87cbd3..7344e07167 100644 --- a/rust/src/trace_context.rs +++ b/rust/src/trace_context.rs @@ -102,31 +102,4 @@ pub(crate) fn inject_trace_context(params: &mut serde_json::Value, ctx: &TraceCo } #[cfg(test)] -mod tests { - use super::TraceContext; - - #[test] - fn new_yields_empty_context() { - let ctx = TraceContext::new(); - assert!(ctx.is_empty()); - assert!(ctx.traceparent.is_none()); - assert!(ctx.tracestate.is_none()); - } - - #[test] - fn builder_composes_traceparent_and_tracestate() { - let ctx = TraceContext::new() - .with_traceparent("00-trace-span-01") - .with_tracestate("vendor=key"); - assert_eq!(ctx.traceparent.as_deref(), Some("00-trace-span-01")); - assert_eq!(ctx.tracestate.as_deref(), Some("vendor=key")); - assert!(!ctx.is_empty()); - } - - #[test] - fn from_traceparent_matches_builder() { - let direct = TraceContext::from_traceparent("00-trace-span-01"); - let chained = TraceContext::new().with_traceparent("00-trace-span-01"); - assert_eq!(direct, chained); - } -} +mod tests; diff --git a/rust/src/trace_context/tests.rs b/rust/src/trace_context/tests.rs new file mode 100644 index 0000000000..ff151b80cb --- /dev/null +++ b/rust/src/trace_context/tests.rs @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::TraceContext; + +#[test] +fn new_yields_empty_context() { + let ctx = TraceContext::new(); + assert!(ctx.is_empty()); + assert!(ctx.traceparent.is_none()); + assert!(ctx.tracestate.is_none()); +} + +#[test] +fn builder_composes_traceparent_and_tracestate() { + let ctx = TraceContext::new() + .with_traceparent("00-trace-span-01") + .with_tracestate("vendor=key"); + assert_eq!(ctx.traceparent.as_deref(), Some("00-trace-span-01")); + assert_eq!(ctx.tracestate.as_deref(), Some("vendor=key")); + assert!(!ctx.is_empty()); +} + +#[test] +fn from_traceparent_matches_builder() { + let direct = TraceContext::from_traceparent("00-trace-span-01"); + let chained = TraceContext::new().with_traceparent("00-trace-span-01"); + assert_eq!(direct, chained); +} diff --git a/rust/src/transforms.rs b/rust/src/transforms.rs index a090bc6494..dc93094c25 100644 --- a/rust/src/transforms.rs +++ b/rust/src/transforms.rs @@ -110,114 +110,4 @@ pub(crate) async fn dispatch_transform( } #[cfg(test)] -mod tests { - use super::*; - - struct TestTransform; - - #[async_trait] - impl SystemMessageTransform for TestTransform { - fn section_ids(&self) -> Vec { - vec!["instructions".to_string(), "context".to_string()] - } - - async fn transform_section( - &self, - section_id: &str, - content: &str, - _ctx: TransformContext, - ) -> Option { - match section_id { - "instructions" => Some(format!("[modified] {content}")), - _ => None, - } - } - } - - #[tokio::test] - async fn dispatch_applies_matching_transform() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "instructions".to_string(), - TransformSection { - content: "be helpful".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!( - response.sections["instructions"].content, - "[modified] be helpful" - ); - } - - #[tokio::test] - async fn dispatch_passes_through_unhandled_section() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "context".to_string(), - TransformSection { - content: "original context".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!(response.sections["context"].content, "original context"); - } - - #[tokio::test] - async fn dispatch_unknown_section_passes_through() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "unknown".to_string(), - TransformSection { - content: "mystery".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!(response.sections["unknown"].content, "mystery"); - } - - #[tokio::test] - async fn dispatch_mixed_sections() { - let transform = TestTransform; - let mut sections = HashMap::new(); - sections.insert( - "instructions".to_string(), - TransformSection { - content: "help me".to_string(), - }, - ); - sections.insert( - "context".to_string(), - TransformSection { - content: "some context".to_string(), - }, - ); - sections.insert( - "other".to_string(), - TransformSection { - content: "other stuff".to_string(), - }, - ); - - let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; - assert_eq!( - response.sections["instructions"].content, - "[modified] help me" - ); - assert_eq!(response.sections["context"].content, "some context"); - assert_eq!(response.sections["other"].content, "other stuff"); - } - - #[tokio::test] - async fn section_ids_returns_registered_sections() { - let transform = TestTransform; - let ids = transform.section_ids(); - assert_eq!(ids, vec!["instructions", "context"]); - } -} +mod tests; diff --git a/rust/src/transforms/tests.rs b/rust/src/transforms/tests.rs new file mode 100644 index 0000000000..c56f804920 --- /dev/null +++ b/rust/src/transforms/tests.rs @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::*; + +struct TestTransform; + +#[async_trait] +impl SystemMessageTransform for TestTransform { + fn section_ids(&self) -> Vec { + vec!["instructions".to_string(), "context".to_string()] + } + + async fn transform_section( + &self, + section_id: &str, + content: &str, + _ctx: TransformContext, + ) -> Option { + match section_id { + "instructions" => Some(format!("[modified] {content}")), + _ => None, + } + } +} + +#[tokio::test] +async fn dispatch_applies_matching_transform() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "instructions".to_string(), + TransformSection { + content: "be helpful".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!( + response.sections["instructions"].content, + "[modified] be helpful" + ); +} + +#[tokio::test] +async fn dispatch_passes_through_unhandled_section() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "context".to_string(), + TransformSection { + content: "original context".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!(response.sections["context"].content, "original context"); +} + +#[tokio::test] +async fn dispatch_unknown_section_passes_through() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "unknown".to_string(), + TransformSection { + content: "mystery".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!(response.sections["unknown"].content, "mystery"); +} + +#[tokio::test] +async fn dispatch_mixed_sections() { + let transform = TestTransform; + let mut sections = HashMap::new(); + sections.insert( + "instructions".to_string(), + TransformSection { + content: "help me".to_string(), + }, + ); + sections.insert( + "context".to_string(), + TransformSection { + content: "some context".to_string(), + }, + ); + sections.insert( + "other".to_string(), + TransformSection { + content: "other stuff".to_string(), + }, + ); + + let response = dispatch_transform(&transform, &SessionId::new("sess-1"), sections).await; + assert_eq!( + response.sections["instructions"].content, + "[modified] help me" + ); + assert_eq!(response.sections["context"].content, "some context"); + assert_eq!(response.sections["other"].content, "other stuff"); +} + +#[tokio::test] +async fn section_ids_returns_registered_sections() { + let transform = TestTransform; + let ids = transform.section_ids(); + assert_eq!(ids, vec!["instructions", "context"]); +} diff --git a/rust/src/types.rs b/rust/src/types.rs index 5c5d5b17b6..7a272cecfc 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -43,11 +43,12 @@ use crate::handler::{ use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; pub use crate::session_fs::{ - DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig, - SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, - SessionFsSqliteQueryType, SessionFsSqliteTransactionError, + DirEntry, DirEntryKind, FileInfo, FsError, SessionFsBinaryProvider, SessionFsCapabilities, + SessionFsConfig, SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, + SessionFsSqliteQueryResult, SessionFsSqliteQueryType, SessionFsSqliteTransactionError, SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement, }; +use crate::skill_provider::SkillProvider; pub use crate::trace_context::{TraceContext, TraceContextProvider}; use crate::transforms::SystemMessageTransform; @@ -532,6 +533,29 @@ impl Tool { } } +/// Handlers for a session's client-supplied tools, keyed by tool name. +pub(crate) type ToolHandlerMap = HashMap>; + +/// Move each tool's handler into a [`ToolHandlerMap`], leaving declaration-only +/// definitions to send on the wire. +/// +/// Two handlers for one name are rejected because dispatch, which is keyed by +/// name, could only ever reach one of them. +pub(crate) fn take_tool_handlers(tools: &mut [Tool]) -> Result { + let mut handlers = ToolHandlerMap::new(); + for tool in tools { + if let Some(handler) = tool.handler.take() + && handlers.insert(tool.name.clone(), handler).is_some() + { + return Err(crate::Error::with_message( + crate::ErrorKind::InvalidConfig, + format!("duplicate tool handler registered for name {:?}", tool.name), + )); + } + } + Ok(handlers) +} + impl std::fmt::Debug for Tool { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Tool") @@ -1245,6 +1269,12 @@ pub struct ProviderConfig { /// Applies to OpenAI-compatible providers using `wire_api` `"responses"`. #[serde(default, skip_serializing_if = "Option::is_none")] pub transport: Option, + /// Product serving the model, such as `"ollama"` or `"lm_studio"`, + /// reported in telemetry as `model_provider`. Allowed values are + /// `"openai"`, `"anthropic"`, `"azure_openai"`, `"ollama"`, + /// `"lm_studio"`, `"foundry_local"`, and `"llama_cpp"`; only affects telemetry. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// API endpoint URL. pub base_url: String, /// API key. Optional for local providers like Ollama. @@ -1298,6 +1328,7 @@ impl std::fmt::Debug for ProviderConfig { .field("provider_type", &self.provider_type) .field("wire_api", &self.wire_api) .field("transport", &self.transport) + .field("model_provider", &self.model_provider) .field("base_url", &self.base_url) .field("api_key", &self.api_key) .field("bearer_token", &self.bearer_token) @@ -1345,6 +1376,14 @@ impl ProviderConfig { self } + /// Set the product serving the model. Allowed values are `"openai"`, + /// `"anthropic"`, `"azure_openai"`, `"ollama"`, `"lm_studio"`, + /// `"foundry_local"`, and `"llama_cpp"`. Only affects telemetry. + pub fn with_model_provider(mut self, model_provider: impl Into) -> Self { + self.model_provider = Some(model_provider.into()); + self + } + /// Set the API key. Optional for local providers like Ollama. pub fn with_api_key(mut self, api_key: impl Into) -> Self { self.api_key = Some(api_key.into()); @@ -1503,6 +1542,12 @@ pub struct NamedProviderConfig { /// Defaults to `"completions"`. #[serde(default, skip_serializing_if = "Option::is_none")] pub wire_api: Option, + /// Product serving this provider's models, such as `"ollama"` or + /// `"lm_studio"`, reported in telemetry as `model_provider`. Allowed values + /// are `"openai"`, `"anthropic"`, `"azure_openai"`, `"ollama"`, + /// `"lm_studio"`, `"foundry_local"`, and `"llama_cpp"`; only affects telemetry. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model_provider: Option, /// API endpoint URL. pub base_url: String, /// API key. Optional for local providers like Ollama. @@ -1532,6 +1577,7 @@ impl std::fmt::Debug for NamedProviderConfig { .field("name", &self.name) .field("provider_type", &self.provider_type) .field("wire_api", &self.wire_api) + .field("model_provider", &self.model_provider) .field("base_url", &self.base_url) .field("api_key", &self.api_key) .field("bearer_token", &self.bearer_token) @@ -1569,6 +1615,14 @@ impl NamedProviderConfig { self } + /// Set the product serving this provider's models. Allowed values are + /// `"openai"`, `"anthropic"`, `"azure_openai"`, `"ollama"`, + /// `"lm_studio"`, `"foundry_local"`, and `"llama_cpp"`. Only affects telemetry. + pub fn with_model_provider(mut self, model_provider: impl Into) -> Self { + self.model_provider = Some(model_provider.into()); + self + } + /// Set the API key. Optional for local providers like Ollama. pub fn with_api_key(mut self, api_key: impl Into) -> Self { self.api_key = Some(api_key.into()); @@ -1827,6 +1881,12 @@ pub struct ManagedSettingsPermissions { /// Tool-permission patterns that are allowed without prompting. #[serde(default, skip_serializing_if = "Option::is_none")] pub allow: Option>, + /// Closed-world host boundary expressed as `Domain(hostname)`, + /// `Domain(IP)`, or `Domain(*.example.com)` rules. Schemes, ports, paths, + /// queries, and fragments are rejected. Multiple managed layers intersect + /// their lists. A present empty list denies all hosts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit_to: Option>, } impl ManagedSettingsPermissions { @@ -1853,6 +1913,12 @@ impl ManagedSettingsPermissions { self.allow = Some(rules); self } + + /// Sets the closed-world domain boundary for this managed layer. + pub fn with_limit_to(mut self, rules: Vec) -> Self { + self.limit_to = Some(rules); + self + } } /// Managed-settings layer injected at session startup. Currently carries only a @@ -2062,6 +2128,13 @@ pub struct SessionConfig { pub enable_session_store: Option, /// When true, enables skills for this session. pub enable_skills: Option, + /// **Experimental.** Supplies session-scoped skills from the SDK host. + /// + /// The provider is runtime-only and is not serialized. Set + /// [`enable_skills`](Self::enable_skills) to `Some(true)` when using this + /// in [`ClientMode::Empty`](crate::ClientMode::Empty), where built-in + /// skill loading is otherwise disabled by default. + pub skill_provider: Option>, /// **Experimental.** This option is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -2381,6 +2454,10 @@ impl std::fmt::Debug for SessionConfig { ) .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) + .field( + "skill_provider", + &self.skill_provider.as_ref().map(|_| ""), + ) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) @@ -2512,6 +2589,7 @@ impl Default for SessionConfig { enable_host_git_operations: None, enable_session_store: None, enable_skills: None, + skill_provider: None, embedding_cache_storage: None, enable_mcp_apps: None, github_mcp_tool_config: None, @@ -2585,9 +2663,10 @@ pub(crate) struct SessionConfigRuntime { pub auto_mode_switch_handler: Option>, pub hooks_handler: Option>, pub system_message_transform: Option>, - pub tool_handlers: HashMap>, + pub tool_handlers: ToolHandlerMap, pub canvas_handler: Option>, pub session_fs_provider: Option>, + pub skill_provider: Option>, pub bearer_token_providers: HashMap>, pub github_token_provider: Option>, pub commands: Option>, @@ -2622,20 +2701,9 @@ impl SessionConfig { let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.is_some(); + let has_skill_provider = self.skill_provider.is_some(); - let mut tool_handlers: HashMap> = HashMap::new(); - if let Some(tools) = self.tools.as_mut() { - for tool in tools.iter_mut() { - if let Some(handler) = tool.handler.take() - && tool_handlers.insert(tool.name.clone(), handler).is_some() - { - return Err(crate::Error::with_message( - crate::ErrorKind::InvalidConfig, - format!("duplicate tool handler registered for name {:?}", tool.name), - )); - } - } - } + let tool_handlers = take_tool_handlers(self.tools.as_deref_mut().unwrap_or_default())?; let wire_commands = self.commands.as_ref().map(|cmds| { cmds.iter() @@ -2687,6 +2755,7 @@ impl SessionConfig { enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, + has_skill_provider: has_skill_provider.then_some(true), request_user_input, request_permission: permission_active, request_exit_plan_mode, @@ -2747,6 +2816,7 @@ impl SessionConfig { tool_handlers, canvas_handler, session_fs_provider: self.session_fs_provider, + skill_provider: self.skill_provider, bearer_token_providers, github_token_provider: self.github_token_provider, commands: self.commands, @@ -3110,6 +3180,14 @@ impl SessionConfig { self } + /// Install an experimental session-scoped [`SkillProvider`]. + /// + /// The provider is not serialized and must be re-supplied on resume. + pub fn with_skill_provider(mut self, provider: Arc) -> Self { + self.skill_provider = Some(provider); + self + } + /// **Experimental.** This method is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -3558,6 +3636,11 @@ pub struct ResumeSessionConfig { pub enable_session_store: Option, /// When true, enables skills on resume. pub enable_skills: Option, + /// **Experimental.** Supplies session-scoped skills from the SDK host. + /// + /// The provider is runtime-only and is not serialized. Re-supply it on + /// each resume because providers are not persisted by the runtime. + pub skill_provider: Option>, /// **Experimental.** This option is part of an experimental wire-protocol /// surface (SEP-1865) and may change or be removed in a future release. /// @@ -3693,9 +3776,15 @@ pub struct ResumeSessionConfig { /// was dropped. Use this together with [`Client::force_stop`] to hand /// off a session from one process to another without losing in-flight /// work. + /// When omitted or `false` (the default), work still pending on resume + /// is treated as interrupted; completed tool results already recorded by + /// the runtime are preserved. /// /// [`Client::force_stop`]: crate::Client::force_stop pub continue_pending_work: Option, + /// Permit recovery of a damaged transcript on resume. Defaults to `true` + /// in all modes when unset. Set `false` to reject recovery. + pub allow_transcript_recovery: Option, /// Optional permission-request handler. See /// [`SessionConfig::permission_handler`]. pub permission_handler: Option>, @@ -3793,6 +3882,10 @@ impl std::fmt::Debug for ResumeSessionConfig { ) .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) + .field( + "skill_provider", + &self.skill_provider.as_ref().map(|_| ""), + ) .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) @@ -3902,20 +3995,9 @@ impl ResumeSessionConfig { let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.is_some(); + let has_skill_provider = self.skill_provider.is_some(); - let mut tool_handlers: HashMap> = HashMap::new(); - if let Some(tools) = self.tools.as_mut() { - for tool in tools.iter_mut() { - if let Some(handler) = tool.handler.take() - && tool_handlers.insert(tool.name.clone(), handler).is_some() - { - return Err(crate::Error::with_message( - crate::ErrorKind::InvalidConfig, - format!("duplicate tool handler registered for name {:?}", tool.name), - )); - } - } - } + let tool_handlers = take_tool_handlers(self.tools.as_deref_mut().unwrap_or_default())?; let wire_commands = self.commands.as_ref().map(|cmds| { cmds.iter() @@ -3967,6 +4049,7 @@ impl ResumeSessionConfig { enable_host_git_operations: self.enable_host_git_operations, enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, + has_skill_provider: has_skill_provider.then_some(true), request_user_input, request_permission: permission_active, request_exit_plan_mode, @@ -4013,6 +4096,7 @@ impl ResumeSessionConfig { managed_settings: self.managed_settings, suppress_resume_event: self.suppress_resume_event, continue_pending_work: self.continue_pending_work, + allow_transcript_recovery: self.allow_transcript_recovery, }; let runtime = SessionConfigRuntime { @@ -4028,6 +4112,7 @@ impl ResumeSessionConfig { tool_handlers, canvas_handler, session_fs_provider: self.session_fs_provider, + skill_provider: self.skill_provider, bearer_token_providers, github_token_provider: self.github_token_provider, commands: self.commands, @@ -4077,6 +4162,7 @@ impl ResumeSessionConfig { enable_host_git_operations: None, enable_session_store: None, enable_skills: None, + skill_provider: None, embedding_cache_storage: None, enable_mcp_apps: None, github_mcp_tool_config: None, @@ -4117,6 +4203,7 @@ impl ResumeSessionConfig { session_fs_provider: None, suppress_resume_event: None, continue_pending_work: None, + allow_transcript_recovery: None, permission_handler: None, elicitation_handler: None, mcp_auth_handler: None, @@ -4211,6 +4298,14 @@ impl ResumeSessionConfig { self } + /// Install an experimental session-scoped [`SkillProvider`]. + /// + /// The provider is not persisted and must be re-supplied on each resume. + pub fn with_skill_provider(mut self, provider: Arc) -> Self { + self.skill_provider = Some(provider); + self + } + /// Auto-approve every permission request on the resumed session. See /// [`SessionConfig::approve_all_permissions`]. pub fn approve_all_permissions(mut self) -> Self { @@ -4727,11 +4822,19 @@ impl ResumeSessionConfig { /// was dropped. Use this together with /// [`Client::force_stop`](crate::Client::force_stop) to hand off a /// session from one process to another without losing in-flight work. + /// When `false` (the default), pending work is treated as interrupted on + /// resume; already-recorded tool results are preserved. pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self { self.continue_pending_work = Some(continue_pending); self } + /// Set [`Self::allow_transcript_recovery`]. + pub fn with_allow_transcript_recovery(mut self, allow: bool) -> Self { + self.allow_transcript_recovery = Some(allow); + self + } + /// Set [`Self::skip_custom_instructions`]. pub fn with_skip_custom_instructions(mut self, value: bool) -> Self { self.skip_custom_instructions = Some(value); @@ -4887,6 +4990,18 @@ pub struct CreateSessionResult { pub capabilities: Option, } +/// Details of transcript repair planned during resume. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TranscriptRecovery { + /// Planned backup path; the backup is written on the next append. + pub planned_backup_path: String, + /// One-based physical line numbers removed from the transcript. + pub invalid_line_numbers: Vec, + /// Whether a valid session.start event was moved to the beginning. + pub session_start_moved: bool, +} + /// Response from `session.resume`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4910,6 +5025,9 @@ pub(crate) struct ResumeSessionResult { skip_serializing_if = "Option::is_none" )] pub open_canvases: Option>, + /// Recovery performed in memory while loading the session. + #[serde(default)] + pub transcript_recovery: Option, } /// Severity level for [`Session::log`](crate::session::Session::log) messages. @@ -6354,2205 +6472,13 @@ impl Default for ExitPlanModeData { } #[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::path::PathBuf; - - use serde_json::json; - - use super::{ - AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, - ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, - ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, - InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, - MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, - ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, - SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, - }; - use crate::generated::session_events::TypedSessionEvent; - - #[test] - fn permission_response_capability_is_publicly_exported() { - assert_eq!( - serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(), - json!("interactive") - ); - } - - #[test] - fn tool_builder_composes() { - let tool = Tool::new("greet") - .with_description("Say hello") - .with_namespaced_name("hello/greet") - .with_instructions("Pass the user's name") - .with_parameters(json!({ - "type": "object", - "properties": { "name": { "type": "string" } }, - "required": ["name"] - })) - .with_overrides_built_in_tool(true) - .with_skip_permission(true); - assert_eq!(tool.name, "greet"); - assert_eq!(tool.description, "Say hello"); - assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet")); - assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name")); - assert_eq!(tool.parameters.get("type").unwrap(), &json!("object")); - assert!(tool.overrides_built_in_tool); - assert!(tool.skip_permission); - } - - #[test] - fn tool_defer_serialization() { - let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto); - assert_eq!(tool.defer, Some(super::DeferMode::Auto)); - let value = serde_json::to_value(&tool).unwrap(); - assert_eq!(value.get("defer").unwrap(), &json!("auto")); - - let plain = Tool::new("plain"); - let value = serde_json::to_value(&plain).unwrap(); - assert!(value.get("defer").is_none()); - } - - #[test] - fn tool_metadata_serialization() { - use indexmap::IndexMap; - - let mut metadata = IndexMap::new(); - metadata.insert( - "github.com/copilot:safeForTelemetry".to_string(), - json!({ "name": true, "inputsNames": false }), - ); - let tool = Tool::new("lookup").with_metadata(metadata); - let value = serde_json::to_value(&tool).unwrap(); - assert_eq!( - value - .get("metadata") - .unwrap() - .get("github.com/copilot:safeForTelemetry") - .unwrap(), - &json!({ "name": true, "inputsNames": false }) - ); - - // Empty metadata is omitted on the wire. - let plain = Tool::new("plain"); - let value = serde_json::to_value(&plain).unwrap(); - assert!(value.get("metadata").is_none()); - } - - #[test] - fn custom_agent_config_builder_with_model() { - let agent = CustomAgentConfig::new("my-agent", "You are helpful.") - .with_model("claude-haiku-4.5") - .with_display_name("My Agent"); - assert_eq!(agent.name, "my-agent"); - assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5")); - assert_eq!(agent.display_name.as_deref(), Some("My Agent")); - } - - #[test] - fn custom_agent_config_serializes_model() { - let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5"); - let wire = serde_json::to_value(&agent).unwrap(); - assert_eq!(wire["model"], "claude-haiku-4.5"); - assert_eq!(wire["name"], "model-agent"); - } - - #[test] - fn custom_agent_config_omits_model_when_none() { - let agent = CustomAgentConfig::new("no-model-agent", "prompt"); - let wire = serde_json::to_value(&agent).unwrap(); - assert!(wire.get("model").is_none()); - } - - #[test] - fn custom_agent_config_builder_with_reasoning_effort() { - let agent = - CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); - assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); - } - - #[test] - fn custom_agent_config_serializes_reasoning_effort() { - let agent = - CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); - let wire = serde_json::to_value(&agent).unwrap(); - assert_eq!(wire["reasoningEffort"], "high"); - } - - #[test] - fn custom_agent_config_omits_reasoning_effort_when_none() { - let agent = CustomAgentConfig::new("default-agent", "prompt"); - let wire = serde_json::to_value(&agent).unwrap(); - assert!(wire.get("reasoningEffort").is_none()); - } - - #[test] - #[should_panic(expected = "tool parameter schema must be a JSON object")] - fn tool_with_parameters_panics_on_non_object_value() { - let _ = Tool::new("noop").with_parameters(json!(null)); - } - - #[test] - fn tool_result_expanded_serializes_binary_results_for_llm() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: "rendered chart".to_string(), - result_type: "success".to_string(), - binary_results_for_llm: Some(vec![ToolBinaryResult { - data: "aW1n".to_string(), - mime_type: "image/png".to_string(), - r#type: "image".to_string(), - description: Some("chart preview".to_string()), - }]), - session_log: None, - error: None, - tool_telemetry: None, - tool_references: None, - }), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!( - wire, - json!({ - "result": { - "textResultForLlm": "rendered chart", - "resultType": "success", - "binaryResultsForLlm": [ - { - "data": "aW1n", - "mimeType": "image/png", - "type": "image", - "description": "chart preview" - } - ] - } - }) - ); - } - - #[test] - fn tool_result_expanded_omits_binary_results_for_llm_when_none() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: "ok".to_string(), - result_type: "success".to_string(), - binary_results_for_llm: None, - session_log: None, - error: None, - tool_telemetry: None, - tool_references: None, - }), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!(wire["result"]["textResultForLlm"], "ok"); - assert!(wire["result"].get("binaryResultsForLlm").is_none()); - } - - #[test] - fn tool_result_expanded_serializes_tool_references() { - let response = ToolResultResponse { - result: ToolResult::Expanded( - ToolResultExpanded::new("found 2 tools", "success") - .with_tool_references(["get_weather", "check_status"]), - ), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!( - wire, - json!({ - "result": { - "textResultForLlm": "found 2 tools", - "resultType": "success", - "toolReferences": ["get_weather", "check_status"] - } - }) - ); - } - - #[test] - fn tool_result_expanded_omits_tool_references_when_none() { - let response = ToolResultResponse { - result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")), - }; - - let wire = serde_json::to_value(&response).unwrap(); - - assert_eq!(wire["result"]["textResultForLlm"], "ok"); - assert!(wire["result"].get("toolReferences").is_none()); - } - - #[test] - fn tool_result_expanded_with_tool_references_accepts_owned_strings() { - // The builder is generic over `Into`, so an owned `Vec` - // must compile and populate the field just like a `&str` array. - let names: Vec = vec!["alpha".to_string(), "beta".to_string()]; - let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names); - - assert_eq!( - expanded.tool_references.as_deref(), - Some(["alpha".to_string(), "beta".to_string()].as_slice()) - ); - } - - #[test] - fn tool_result_expanded_deserializes_tool_references() { - let wire = json!({ - "textResultForLlm": "found tools", - "resultType": "success", - "toolReferences": ["alpha", "beta"] - }); - - let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap(); - - assert_eq!( - expanded.tool_references.as_deref(), - Some(["alpha".to_string(), "beta".to_string()].as_slice()) - ); - } - - #[test] - fn session_config_default_wire_flags_off_without_handlers() { - let cfg = SessionConfig::default(); - assert_eq!(cfg.mcp_oauth_token_storage, None); - assert_eq!(cfg.allowed_models, None); - // Wire flags are derived from handler presence at create_session - // time, not stored on the config. With no handlers installed, every - // request_* flag should serialize as false. - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("default-flags"))) - .expect("default config has no duplicate handlers"); - assert!(!wire.request_user_input); - assert!(!wire.request_permission); - assert!(!wire.request_elicitation); - assert!(!wire.request_exit_plan_mode); - assert!(!wire.request_auto_mode_switch); - assert!(!wire.hooks); - assert!(!wire.request_mcp_apps); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("askUserVariant").is_none()); - assert!(json.get("allowedModels").is_none()); - } - - #[test] - fn resume_session_config_new_wire_flags_off_without_handlers() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags")); - assert_eq!(cfg.mcp_oauth_token_storage, None); - assert_eq!(cfg.allowed_models, None); - let (wire, _runtime) = cfg - .into_wire() - .expect("default resume config has no duplicate handlers"); - assert!(!wire.request_user_input); - assert!(!wire.request_permission); - assert!(!wire.request_elicitation); - assert!(!wire.request_exit_plan_mode); - assert!(!wire.request_auto_mode_switch); - assert!(!wire.hooks); - assert!(!wire.request_mcp_apps); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("askUserVariant").is_none()); - assert!(json.get("allowedModels").is_none()); - } - - #[test] - fn session_configs_build_debug_and_serialize_allowed_models() { - let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]); - assert_eq!( - create.allowed_models.as_deref(), - Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..]) - ); - assert!(format!("{create:?}").contains("allowed_models")); - - let (create_wire, _) = create - .into_wire(Some(SessionId::from("create-allowed-models"))) - .expect("allowed model config has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!( - create_json["allowedModels"], - json!(["gpt-5.4", "claude-sonnet-4"]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models")) - .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]); - assert_eq!( - resume.allowed_models.as_deref(), - Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..]) - ); - assert!(format!("{resume:?}").contains("allowed_models")); - - let (resume_wire, _) = resume - .into_wire() - .expect("resume allowed model config has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!( - resume_json["allowedModels"], - json!(["gpt-5.4", "gpt-5-mini"]) - ); - } - - #[test] - fn custom_agents_local_only_serializes_on_create_and_resume() { - let (create_wire, _) = SessionConfig::default() - .with_custom_agents_local_only(false) - .into_wire(Some(SessionId::from("create-locality"))) - .expect("create config has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["customAgentsLocalOnly"], false); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality")) - .with_custom_agents_local_only(false) - .into_wire() - .expect("resume config has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["customAgentsLocalOnly"], false); - - let (unset_create_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("create-unset"))) - .expect("create config has no duplicate handlers"); - let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); - assert!(unset_create_json.get("customAgentsLocalOnly").is_none()); - - let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset")) - .into_wire() - .expect("resume config has no duplicate handlers"); - let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); - assert!(unset_resume_json.get("customAgentsLocalOnly").is_none()); - } - - #[test] - fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { - let cfg = SessionConfig::default().with_enable_mcp_apps(true); - assert_eq!(cfg.enable_mcp_apps, Some(true)); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("enable-mcp-apps"))) - .expect("enable_mcp_apps config has no duplicate handlers"); - assert!(wire.request_mcp_apps); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); - } - - #[test] - fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps")) - .with_enable_mcp_apps(true); - assert_eq!(cfg.enable_mcp_apps, Some(true)); - - let (wire, _runtime) = cfg - .into_wire() - .expect("resume enable_mcp_apps config has no duplicate handlers"); - assert!(wire.request_mcp_apps); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); - } - - #[test] - fn github_mcp_tool_config_serializes_for_create_and_resume() { - let github_config = GitHubMcpToolConfig::new() - .with_enable_all_tools(true) - .with_additional_toolsets(["repos"]) - .with_additional_tools(["get_issue"]) - .with_enable_insiders_mode(true) - .with_disable_form_deferral(true); - - let (create_wire, _) = SessionConfig::default() - .with_github_mcp_tool_config(github_config.clone()) - .into_wire(Some(SessionId::from("github-mcp"))) - .expect("create config has no duplicate handlers"); - assert_eq!( - serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"], - serde_json::json!({ - "enableAllTools": true, - "additionalToolsets": ["repos"], - "additionalTools": ["get_issue"], - "enableInsidersMode": true, - "disableFormDeferral": true, - }) - ); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp")) - .with_github_mcp_tool_config(github_config) - .into_wire() - .expect("resume config has no duplicate handlers"); - assert!(resume_wire.github_mcp_tool_config.is_some()); - - let (unset_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("github-mcp-unset"))) - .expect("default config has no duplicate handlers"); - assert!( - serde_json::to_value(&unset_wire) - .unwrap() - .get("githubMcpToolConfig") - .is_none() - ); - } - - #[test] - fn memory_configuration_constructors_and_serde() { - assert!(MemoryConfiguration::enabled().enabled); - assert!(!MemoryConfiguration::disabled().enabled); - assert!(MemoryConfiguration::disabled().with_enabled(true).enabled); - - let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap(); - assert_eq!(json, serde_json::json!({ "enabled": true })); - } - - #[test] - fn session_config_with_memory_serializes() { - let (wire, _runtime) = SessionConfig::default() - .with_memory(MemoryConfiguration::enabled()) - .into_wire(Some(SessionId::from("memory-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); - - let (wire_off, _) = SessionConfig::default() - .with_memory(MemoryConfiguration::disabled()) - .into_wire(Some(SessionId::from("memory-off"))) - .expect("no duplicate handlers"); - let json_off = serde_json::to_value(&wire_off).unwrap(); - assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false })); - - // Unset memory is omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("memory-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("memory").is_none()); - } - - #[test] - fn resume_session_config_with_memory_serializes() { - let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on")) - .with_memory(MemoryConfiguration::enabled()) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); - - // Unset memory is omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("memory").is_none()); - } - - #[test] - fn feature_flags_serialize_on_create_and_resume() { - let feature_flags = HashMap::from([ - ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), - ("DISABLED_TEST_FLAG".to_string(), false), - ]); - let expected = serde_json::json!({ - "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, - "DISABLED_TEST_FLAG": false, - }); - - let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); - assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); - let (create_wire, _) = create_config - .into_wire(Some(SessionId::from("feature-flags-create"))) - .expect("no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["featureFlags"], expected); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) - .with_feature_flags(feature_flags) - .into_wire() - .expect("no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["featureFlags"], expected); - - let (unset_create_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("feature-flags-create-unset"))) - .expect("no duplicate handlers"); - let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); - assert!(unset_create_json.get("featureFlags").is_none()); - - let (unset_resume_wire, _) = - ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) - .into_wire() - .expect("no duplicate handlers"); - let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); - assert!(unset_resume_json.get("featureFlags").is_none()); - } - - fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { - CopilotExpAssignmentResponse { - features: vec!["copilot_exp_flag".to_string()], - flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]), - configs: vec![ExpConfigEntry { - id: "cfg-1".to_string(), - parameters: HashMap::from([ - ("threshold".to_string(), ExpFlagValue::Integer(5)), - ("enabled".to_string(), ExpFlagValue::Bool(true)), - ]), - }], - assignment_context: context.to_string(), - ..Default::default() - } - } - - #[test] - fn exp_flag_value_round_trips_all_variants() { - let values = serde_json::json!({ - "s": "text", - "i": 7, - "f": 1.5, - "b": true, - "n": null, - }); - let parsed: HashMap = serde_json::from_value(values.clone()).unwrap(); - assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string())); - assert_eq!(parsed["i"], ExpFlagValue::Integer(7)); - assert_eq!(parsed["f"], ExpFlagValue::Float(1.5)); - assert_eq!(parsed["b"], ExpFlagValue::Bool(true)); - assert_eq!(parsed["n"], ExpFlagValue::Null); - assert_eq!(serde_json::to_value(&parsed).unwrap(), values); - } - - #[test] - fn session_config_with_exp_assignments_serializes() { - let assignments = sample_exp_assignments("ctx-123"); - let expected = serde_json::to_value(&assignments).unwrap(); - let (wire, _runtime) = SessionConfig::default() - .with_exp_assignments(assignments) - .into_wire(Some(SessionId::from("exp-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["expAssignments"], expected); - assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123"); - assert_eq!( - json["expAssignments"]["Flights"]["copilot_exp_flag"], - "treatment" - ); - - // Unset exp assignments are omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("exp-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("expAssignments").is_none()); - } - - #[test] - fn resume_session_config_with_exp_assignments_serializes() { - let assignments = sample_exp_assignments("ctx-456"); - let expected = serde_json::to_value(&assignments).unwrap(); - let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on")) - .with_exp_assignments(assignments) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["expAssignments"], expected); - - // Unset exp assignments are omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("expAssignments").is_none()); - } - - #[test] - fn session_config_clone_preserves_exp_assignments() { - let assignments = sample_exp_assignments("ctx-clone"); - let config = SessionConfig::default().with_exp_assignments(assignments.clone()); - let cloned = config.clone(); - - assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); - - let (wire, _runtime) = cloned - .into_wire(Some(SessionId::from("exp-clone"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["expAssignments"], - serde_json::to_value(&assignments).unwrap() - ); - } - - #[test] - fn resume_session_config_clone_preserves_exp_assignments() { - let assignments = sample_exp_assignments("ctx-clone-resume"); - let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone")) - .with_exp_assignments(assignments.clone()); - let cloned = config.clone(); - - assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); - - let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["expAssignments"], - serde_json::to_value(&assignments).unwrap() - ); - } - - #[test] - #[allow(clippy::field_reassign_with_default)] - fn session_config_into_wire_serializes_bucket_b_fields() { - use std::path::PathBuf; - - use super::{CloudSessionOptions, CloudSessionRepository}; - - let mut cfg = SessionConfig::default(); - cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); - cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.github_token = Some("ghs_secret".to_string()); - cfg.include_sub_agent_streaming_events = Some(false); - cfg.enable_session_telemetry = Some(false); - cfg.reasoning_summary = Some(ReasoningSummary::Concise); - cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); - cfg.enable_on_demand_instruction_discovery = Some(false); - cfg.cloud = Some(CloudSessionOptions::with_repository( - CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), - )); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("custom-id"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["sessionId"], "custom-id"); - assert_eq!(wire_json["configDir"], "/tmp/cfg"); - assert_eq!(wire_json["workingDirectory"], "/tmp/work"); - assert_eq!(wire_json["gitHubToken"], "ghs_secret"); - assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); - assert_eq!(wire_json["enableSessionTelemetry"], false); - assert_eq!(wire_json["reasoningSummary"], "concise"); - assert_eq!(wire_json["remoteSession"], "export"); - assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); - assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); - assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); - assert_eq!(wire_json["cloud"]["repository"]["branch"], "main"); - - // Unset fields are omitted on the wire. - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("gitHubToken").is_none()); - assert!(empty_json.get("enableSessionTelemetry").is_none()); - assert!(empty_json.get("reasoningSummary").is_none()); - assert!(empty_json.get("remoteSession").is_none()); - assert!( - empty_json - .get("enableOnDemandInstructionDiscovery") - .is_none() - ); - assert!(empty_json.get("cloud").is_none()); - } - - #[test] - fn session_config_into_wire_serializes_named_providers_and_models() { - let cfg = SessionConfig::default() - .with_providers(vec![ - NamedProviderConfig::new("my-openai", "https://api.example.com/v1") - .with_provider_type("openai") - .with_wire_api("responses") - .with_api_key("sk-test"), - ]) - .with_models(vec![ - ProviderModelConfig::new("gpt-x", "my-openai") - .with_wire_model("gpt-x-2025") - .with_max_output_tokens(2048), - ]); - - let (wire, _) = cfg - .into_wire(Some(SessionId::from("sess-providers"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["providers"][0]["name"], "my-openai"); - assert_eq!( - wire_json["providers"][0]["baseUrl"], - "https://api.example.com/v1" - ); - assert_eq!(wire_json["providers"][0]["type"], "openai"); - assert_eq!(wire_json["providers"][0]["wireApi"], "responses"); - assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test"); - assert_eq!(wire_json["models"][0]["id"], "gpt-x"); - assert_eq!(wire_json["models"][0]["provider"], "my-openai"); - assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025"); - assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("providers").is_none()); - assert!(empty_json.get("models").is_none()); - } - - #[test] - fn resume_config_into_wire_serializes_named_providers_and_models() { - let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume")) - .with_providers(vec![ - NamedProviderConfig::new("my-azure", "https://example.openai.azure.com") - .with_provider_type("azure") - .with_azure(AzureProviderOptions { - api_version: Some("2024-10-21".to_string()), - }), - ]) - .with_models(vec![ - ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"), - ]); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["providers"][0]["name"], "my-azure"); - assert_eq!(wire_json["providers"][0]["type"], "azure"); - assert_eq!( - wire_json["providers"][0]["azure"]["apiVersion"], - "2024-10-21" - ); - assert_eq!(wire_json["models"][0]["id"], "deploy-1"); - assert_eq!(wire_json["models"][0]["provider"], "my-azure"); - assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o"); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty")) - .into_wire() - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("providers").is_none()); - assert!(empty_json.get("models").is_none()); - } - - #[test] - fn session_config_into_wire_serializes_plugin_directories_and_large_output() { - use std::path::PathBuf; - - let cfg = SessionConfig { - plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), - disabled_mcp_servers: Some(vec![ - "local-files".to_string(), - "remote-github".to_string(), - ]), - large_output: Some( - LargeToolOutputConfig::new() - .with_enabled(true) - .with_max_size_bytes(1024) - .with_output_directory(PathBuf::from("/tmp/large-output")), - ), - ..Default::default() - }; - - let (wire, _) = cfg - .into_wire(Some(SessionId::from("sess-1"))) - .expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); - assert_eq!( - wire_json["disabledMcpServers"], - serde_json::json!(["local-files", "remote-github"]) - ); - assert_eq!(wire_json["largeOutput"]["enabled"], true); - assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); - assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("empty"))) - .expect("default has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("pluginDirectories").is_none()); - assert!(empty_json.get("disabledMcpServers").is_none()); - assert!(empty_json.get("largeOutput").is_none()); - } - - #[test] - fn resume_session_config_into_wire_serializes_bucket_b_fields() { - use std::path::PathBuf; - - let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); - cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); - cfg.github_token = Some("ghs_secret".to_string()); - cfg.include_sub_agent_streaming_events = Some(true); - cfg.enable_session_telemetry = Some(false); - cfg.reasoning_summary = Some(ReasoningSummary::Detailed); - cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); - cfg.enable_on_demand_instruction_discovery = Some(false); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["sessionId"], "sess-1"); - assert_eq!(wire_json["workingDirectory"], "/tmp/work"); - assert_eq!(wire_json["configDir"], "/tmp/cfg"); - assert_eq!(wire_json["gitHubToken"], "ghs_secret"); - assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); - assert_eq!(wire_json["enableSessionTelemetry"], false); - assert_eq!(wire_json["reasoningSummary"], "detailed"); - assert_eq!(wire_json["remoteSession"], "on"); - assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); - - // Unset remote_session is omitted on the wire. - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("reasoningSummary").is_none()); - assert!(empty_json.get("remoteSession").is_none()); - assert!( - empty_json - .get("enableOnDemandInstructionDiscovery") - .is_none() - ); - } - - #[test] - fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() { - use std::path::PathBuf; - - let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); - cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); - cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]); - cfg.large_output = Some( - LargeToolOutputConfig::new() - .with_enabled(false) - .with_max_size_bytes(2048) - .with_output_directory(PathBuf::from("/tmp/large-output-r")), - ); - - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let wire_json = serde_json::to_value(&wire).unwrap(); - assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); - assert_eq!( - wire_json["disabledMcpServers"], - serde_json::json!(["local-files-r"]) - ); - assert_eq!(wire_json["largeOutput"]["enabled"], false); - assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); - assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("pluginDirectories").is_none()); - assert!(empty_json.get("disabledMcpServers").is_none()); - assert!(empty_json.get("largeOutput").is_none()); - } - - #[test] - fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() { - let url = "https://example.com/oauth/client-metadata.json"; - - let (create_wire, _) = SessionConfig::default() - .with_auth_client_id_metadata_url(url) - .into_wire(None) - .expect("default create has no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!(create_json["authClientIdMetadataUrl"], url); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_auth_client_id_metadata_url(url) - .into_wire() - .expect("default resume has no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!(resume_json["authClientIdMetadataUrl"], url); - - let (empty_create_wire, _) = SessionConfig::default() - .into_wire(None) - .expect("default create has no duplicate handlers"); - let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap(); - assert!(empty_create_json.get("authClientIdMetadataUrl").is_none()); - - let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("default resume has no duplicate handlers"); - let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap(); - assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none()); - } - - #[test] - fn session_config_clones_disabled_mcp_servers() { - let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); - let mut create_clone = create.clone(); - create_clone - .disabled_mcp_servers - .as_mut() - .expect("configured disabled MCP servers") - .push("remote-github".to_string()); - assert_eq!( - create.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_disabled_mcp_servers(["local-files"]); - let mut resume_clone = resume.clone(); - resume_clone - .disabled_mcp_servers - .as_mut() - .expect("configured disabled MCP servers") - .push("remote-github".to_string()); - assert_eq!( - resume.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - } - - #[test] - fn session_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = SessionConfig::default() - .with_session_id(SessionId::from("sess-1")) - .with_model("claude-sonnet-4") - .with_client_name("test-app") - .with_reasoning_effort("medium") - .with_reasoning_summary(ReasoningSummary::Concise) - .with_context_tier("long_context") - .with_streaming(true) - .with_tools([Tool::new("greet")]) - .with_available_tools(["bash", "view"]) - .with_excluded_tools(["dangerous"]) - .with_mcp_servers(IndexMap::new()) - .with_mcp_oauth_token_storage("persistent") - .with_enable_config_discovery(true) - .with_enable_on_demand_instruction_discovery(true) - .with_skill_directories([PathBuf::from("/tmp/skills")]) - .with_disabled_skills(["broken-skill"]) - .with_disabled_mcp_servers(["local-files"]) - .with_agent("researcher") - .with_config_directory(PathBuf::from("/tmp/config")) - .with_working_directory(PathBuf::from("/tmp/work")) - .with_additional_directories([PathBuf::from("/tmp/shared")]) - .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .with_enable_session_telemetry(false) - .with_include_sub_agent_streaming_events(false) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); - - assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); - assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); - assert_eq!(cfg.client_name.as_deref(), Some("test-app")); - assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium")); - assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise)); - assert_eq!(cfg.context_tier.as_deref(), Some("long_context")); - assert_eq!(cfg.streaming, Some(true)); - assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); - assert_eq!( - cfg.available_tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert_eq!( - cfg.excluded_tools.as_deref(), - Some(&["dangerous".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); - assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true)); - assert_eq!( - cfg.skill_directories.as_deref(), - Some(&[PathBuf::from("/tmp/skills")][..]) - ); - assert_eq!( - cfg.disabled_skills.as_deref(), - Some(&["broken-skill".to_string()][..]) - ); - assert_eq!( - cfg.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); - assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); - assert_eq!( - cfg.additional_directories.as_deref(), - Some(&[PathBuf::from("/tmp/shared")][..]) - ); - assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); - assert_eq!( - cfg.capi, - Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - ); - assert_eq!(cfg.enable_session_telemetry, Some(false)); - assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); - assert_eq!( - cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) - ); - } - - #[test] - fn resume_session_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) - .with_client_name("test-app") - .with_reasoning_summary(ReasoningSummary::None) - .with_context_tier("default") - .with_streaming(true) - .with_tools([Tool::new("greet")]) - .with_available_tools(["bash", "view"]) - .with_excluded_tools(["dangerous"]) - .with_mcp_servers(IndexMap::new()) - .with_mcp_oauth_token_storage("persistent") - .with_enable_config_discovery(true) - .with_enable_on_demand_instruction_discovery(false) - .with_skill_directories([PathBuf::from("/tmp/skills")]) - .with_disabled_skills(["broken-skill"]) - .with_disabled_mcp_servers(["local-files"]) - .with_agent("researcher") - .with_config_directory(PathBuf::from("/tmp/config")) - .with_working_directory(PathBuf::from("/tmp/work")) - .with_additional_directories([PathBuf::from("/tmp/shared")]) - .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .with_enable_session_telemetry(false) - .with_include_sub_agent_streaming_events(true) - .with_suppress_resume_event(true) - .with_continue_pending_work(true) - .with_extension_info(ExtensionInfo::new("github-app", "counter")); - - assert_eq!(cfg.session_id.as_str(), "sess-2"); - assert_eq!(cfg.client_name.as_deref(), Some("test-app")); - assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None)); - assert_eq!(cfg.context_tier.as_deref(), Some("default")); - assert_eq!(cfg.streaming, Some(true)); - assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); - assert_eq!( - cfg.available_tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert_eq!( - cfg.excluded_tools.as_deref(), - Some(&["dangerous".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); - assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false)); - assert_eq!( - cfg.skill_directories.as_deref(), - Some(&[PathBuf::from("/tmp/skills")][..]) - ); - assert_eq!( - cfg.disabled_skills.as_deref(), - Some(&["broken-skill".to_string()][..]) - ); - assert_eq!( - cfg.disabled_mcp_servers.as_deref(), - Some(&["local-files".to_string()][..]) - ); - assert_eq!(cfg.agent.as_deref(), Some("researcher")); - assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); - assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); - assert_eq!( - cfg.additional_directories.as_deref(), - Some(&[PathBuf::from("/tmp/shared")][..]) - ); - assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); - assert_eq!( - cfg.capi, - Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - ); - assert_eq!(cfg.enable_session_telemetry, Some(false)); - assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); - assert_eq!(cfg.suppress_resume_event, Some(true)); - assert_eq!(cfg.continue_pending_work, Some(true)); - assert_eq!( - cfg.extension_info, - Some(ExtensionInfo::new("github-app", "counter")) - ); - } - - /// `continue_pending_work` must serialize to wire as `continuePendingWork` - /// — the runtime keys off this exact field name to opt into the - /// pending-work-handoff pattern. - #[test] - fn resume_session_config_serializes_continue_pending_work_to_camel_case() { - let cfg = - ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["continuePendingWork"], true); - - // Unset case — skip_serializing_if must omit the field. - let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("continuePendingWork").is_none()); - } - - #[test] - fn session_configs_serialize_additional_directories() { - let create = SessionConfig::default().with_additional_directories([ - PathBuf::from("/tmp/shared"), - PathBuf::from("/tmp/generated"), - ]); - let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers"); - let create_json = serde_json::to_value(&create_wire).unwrap(); - assert_eq!( - create_json["additionalDirectories"], - serde_json::json!(["/tmp/shared", "/tmp/generated"]) - ); - - let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_additional_directories([PathBuf::from("/tmp/resumed")]); - let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers"); - let resume_json = serde_json::to_value(&resume_wire).unwrap(); - assert_eq!( - resume_json["additionalDirectories"], - serde_json::json!(["/tmp/resumed"]) - ); - } - - /// The Rust field is `suppress_resume_event`, but the wire field stays - /// `disableResume` to preserve compatibility with the runtime and other - /// SDKs. - #[test] - fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() { - let cfg = - ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["disableResume"], true); - assert!(json.get("suppressResumeEvent").is_none()); - } - - /// `instruction_directories` must serialize to wire as - /// `instructionDirectories` on `SessionConfig`. - #[test] - fn session_config_serializes_instruction_directories_to_camel_case() { - let cfg = - SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]); - let (wire, _) = cfg - .into_wire(Some(SessionId::from("instr-on"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["instructionDirectories"], - serde_json::json!(["/tmp/instr"]) - ); - - // Unset case — skip_serializing_if must omit the field. - let (wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("instr-off"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("instructionDirectories").is_none()); - } - - /// Same check on the resume path. Forwarded to the CLI on - /// `session.resume`. - #[test] - fn resume_session_config_serializes_instruction_directories_to_camel_case() { - let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")) - .with_instruction_directories([PathBuf::from("/tmp/instr")]); - let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["instructionDirectories"], - serde_json::json!(["/tmp/instr"]) - ); - - let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("instructionDirectories").is_none()); - } - - #[test] - fn custom_agent_config_builder_composes() { - use indexmap::IndexMap; - - let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.") - .with_display_name("Research Assistant") - .with_description("Investigates technical questions.") - .with_tools(["bash", "view"]) - .with_mcp_servers(IndexMap::new()) - .with_infer(true) - .with_skills(["rust-coding-skill"]); - - assert_eq!(cfg.name, "researcher"); - assert_eq!(cfg.prompt, "You are a research assistant."); - assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant")); - assert_eq!( - cfg.description.as_deref(), - Some("Investigates technical questions.") - ); - assert_eq!( - cfg.tools.as_deref(), - Some(&["bash".to_string(), "view".to_string()][..]) - ); - assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.infer, Some(true)); - assert_eq!( - cfg.skills.as_deref(), - Some(&["rust-coding-skill".to_string()][..]) - ); - } - - #[test] - fn mcp_servers_serialize_in_insertion_order() { - use indexmap::IndexMap; - - // Regression: `mcp_servers` was a `HashMap`, so the server keys (and - // thus the `session.create` payload) serialized in a per-process - // random order; `IndexMap` pins them to insertion order. The long - // sequence makes a `HashMap` regression reproduce this exact order by - // chance only 1/N!, avoiding a flaky false pass. - let order = [ - "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon", - "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet", - ]; - let mut servers = IndexMap::new(); - for name in order { - servers.insert( - name.to_string(), - McpServerConfig::Stdio(McpStdioServerConfig { - command: "run".to_string(), - ..Default::default() - }), - ); - } - - let (wire, _runtime) = SessionConfig::default() - .with_mcp_servers(servers) - .into_wire(None) - .expect("into_wire should succeed"); - let json = serde_json::to_string(&wire).expect("serialize wire"); - - let positions: Vec = order - .iter() - .map(|name| { - json.find(&format!("\"{name}\"")) - .unwrap_or_else(|| panic!("server {name} missing from wire JSON")) - }) - .collect(); - let mut ascending = positions.clone(); - ascending.sort_unstable(); - assert_eq!( - positions, ascending, - "mcp server keys must serialize in insertion order: {json}" - ); - } - - #[test] - fn infinite_session_config_builder_composes() { - let cfg = InfiniteSessionConfig::new() - .with_enabled(true) - .with_background_compaction_threshold(0.75) - .with_buffer_exhaustion_threshold(0.92); - - assert_eq!(cfg.enabled, Some(true)); - assert_eq!(cfg.background_compaction_threshold, Some(0.75)); - assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92)); - } - - #[test] - fn provider_config_builder_composes() { - use std::collections::HashMap; - - let mut headers = HashMap::new(); - headers.insert("X-Custom".to_string(), "value".to_string()); - - let cfg = ProviderConfig::new("https://api.example.com") - .with_provider_type("openai") - .with_wire_api("completions") - .with_transport("websockets") - .with_api_key("sk-test") - .with_bearer_token("bearer-test") - .with_headers(headers) - .with_model_id("gpt-4") - .with_wire_model("azure-gpt-4-deployment") - .with_max_prompt_tokens(8192) - .with_max_output_tokens(2048); - - assert_eq!(cfg.base_url, "https://api.example.com"); - assert_eq!(cfg.provider_type.as_deref(), Some("openai")); - assert_eq!(cfg.wire_api.as_deref(), Some("completions")); - assert_eq!(cfg.transport.as_deref(), Some("websockets")); - assert_eq!(cfg.api_key.as_deref(), Some("sk-test")); - assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test")); - assert_eq!( - cfg.headers - .as_ref() - .and_then(|h| h.get("X-Custom")) - .map(String::as_str), - Some("value"), - ); - assert_eq!(cfg.model_id.as_deref(), Some("gpt-4")); - assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment")); - assert_eq!(cfg.max_prompt_tokens, Some(8192)); - assert_eq!(cfg.max_output_tokens, Some(2048)); - - // Wire-shape: camelCase, skip_serializing_if when unset. - let wire = serde_json::to_value(&cfg).unwrap(); - assert_eq!(wire["modelId"], "gpt-4"); - assert_eq!(wire["wireModel"], "azure-gpt-4-deployment"); - assert_eq!(wire["maxPromptTokens"], 8192); - assert_eq!(wire["maxOutputTokens"], 2048); - - let unset = ProviderConfig::new("https://api.example.com"); - let wire_unset = serde_json::to_value(&unset).unwrap(); - assert!(wire_unset.get("modelId").is_none()); - assert!(wire_unset.get("wireModel").is_none()); - assert!(wire_unset.get("maxPromptTokens").is_none()); - assert!(wire_unset.get("maxOutputTokens").is_none()); - } - - #[test] - fn capi_session_options_builder_composes_and_serializes() { - let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false); - - assert_eq!(cfg.enable_web_socket_responses, Some(false)); - - let wire = serde_json::to_value(&cfg).unwrap(); - assert_eq!( - wire, - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let unset = CapiSessionOptions::new(); - let wire_unset = serde_json::to_value(&unset).unwrap(); - assert!(wire_unset.get("enableWebSocketResponses").is_none()); - assert!(wire_unset.get("autoTier").is_none()); - assert_eq!(wire_unset, json!({})); - } - - #[test] - fn capi_auto_tier_canonical_values_round_trip_and_forward() { - for (tier, value) in [ - (AutoTier::Efficiency, "efficiency"), - (AutoTier::Balance, "balance"), - (AutoTier::Intelligence, "intelligence"), - (AutoTier::Fast, "fast"), - ] { - let exported: crate::AutoTier = tier.clone(); - let capi = CapiSessionOptions::new().with_auto_tier(exported); - assert_eq!(capi.auto_tier, Some(tier)); - assert_eq!( - serde_json::to_value(&capi).unwrap(), - json!({"autoTier": value}) - ); - assert_eq!( - serde_json::from_value::(json!({"autoTier": value})).unwrap(), - capi - ); - - let capi = capi.with_enable_web_socket_responses(false); - let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); - let (create, _) = SessionConfig::default() - .with_model("auto") - .with_capi(capi.clone()) - .into_wire(Some(SessionId::from("capi-create"))) - .unwrap(); - assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); - - let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) - .with_capi(capi) - .into_wire() - .unwrap(); - assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); - } - } - - #[test] - fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { - for value in ["balanced", "Balance", "unknown"] { - assert_eq!( - serde_json::from_value::(json!(value)).unwrap(), - AutoTier::Unknown - ); - } - let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); - assert_eq!(capi.auto_tier, None); - } - - #[test] - fn session_config_with_capi_serializes() { - let (wire, _) = SessionConfig::default() - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .into_wire(Some(SessionId::from("capi-create"))) - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["capi"], - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let (empty_wire, _) = SessionConfig::default() - .into_wire(Some(SessionId::from("capi-create-unset"))) - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("capi").is_none()); - } - - #[test] - fn resume_session_config_with_capi_serializes() { - let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) - .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) - .into_wire() - .expect("no duplicate handlers"); - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!( - json["capi"], - serde_json::json!({ "enableWebSocketResponses": false }) - ); - - let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset")) - .into_wire() - .expect("no duplicate handlers"); - let empty_json = serde_json::to_value(&empty_wire).unwrap(); - assert!(empty_json.get("capi").is_none()); - } - - #[test] - fn system_message_config_builder_composes() { - use std::collections::HashMap; - - let cfg = SystemMessageConfig::new() - .with_mode("replace") - .with_content("Custom system message.") - .with_sections(HashMap::new()); - - assert_eq!(cfg.mode.as_deref(), Some("replace")); - assert_eq!(cfg.content.as_deref(), Some("Custom system message.")); - assert!(cfg.sections.is_some()); - } - - #[test] - fn delivery_mode_serializes_to_kebab_case_strings() { - assert_eq!( - serde_json::to_string(&DeliveryMode::Enqueue).unwrap(), - "\"enqueue\"" - ); - assert_eq!( - serde_json::to_string(&DeliveryMode::Immediate).unwrap(), - "\"immediate\"" - ); - let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap(); - assert_eq!(parsed, DeliveryMode::Immediate); - } - - #[test] - fn agent_mode_serializes_to_kebab_case_strings() { - assert_eq!( - serde_json::to_string(&AgentMode::Interactive).unwrap(), - "\"interactive\"" - ); - assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\""); - assert_eq!( - serde_json::to_string(&AgentMode::Autopilot).unwrap(), - "\"autopilot\"" - ); - assert_eq!( - serde_json::to_string(&AgentMode::Shell).unwrap(), - "\"shell\"" - ); - let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap(); - assert_eq!(parsed, AgentMode::Plan); - } - - #[test] - fn connection_state_distinguishes_variants() { - // ConnectionState is now an internal type; verify we can construct - // and compare the variants used by the lifecycle code paths. - assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected); - } - - /// `agentId` is the sub-agent attribution field added in copilot-sdk - /// commit f8cf846 ("Derive session event envelopes from schema"). - /// Every other SDK (Node, Python, Go, .NET) carries it on the event - /// envelope; Rust must too or sub-agent events lose attribution at - /// the deserialization boundary. Cross-SDK parity test. - #[test] - fn session_event_round_trips_agent_id_on_envelope() { - let wire = json!({ - "id": "evt-1", - "timestamp": "2026-04-30T12:00:00Z", - "parentId": null, - "agentId": "sub-agent-42", - "type": "assistant.message", - "data": { "message": "hi" } - }); - - let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap(); - assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); - - // Round-trip preserves the field on the wire. - let roundtripped = serde_json::to_value(&event).unwrap(); - assert_eq!(roundtripped["agentId"], "sub-agent-42"); - - // Absent agentId remains absent (skip_serializing_if). - let main_agent_event: SessionEvent = serde_json::from_value(json!({ - "id": "evt-2", - "timestamp": "2026-04-30T12:00:01Z", - "parentId": null, - "type": "session.idle", - "data": {} - })) - .unwrap(); - assert!(main_agent_event.agent_id.is_none()); - let roundtripped = serde_json::to_value(&main_agent_event).unwrap(); - assert!(roundtripped.get("agentId").is_none()); - } - - /// Same parity for the typed event envelope produced by the codegen. - #[test] - fn typed_session_event_round_trips_agent_id_on_envelope() { - let wire = json!({ - "id": "evt-1", - "timestamp": "2026-04-30T12:00:00Z", - "parentId": null, - "agentId": "sub-agent-42", - "type": "session.idle", - "data": {} - }); - - let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); - assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); - - let roundtripped = serde_json::to_value(&event).unwrap(); - assert_eq!(roundtripped["agentId"], "sub-agent-42"); - } - - #[test] - fn connection_state_variants_compile() { - // Defensive smoke test: all variants must be constructable from - // within the crate. (The enum was demoted from pub to pub(crate) - // in Phase D; this test guards against accidental removal.) - let _ = ConnectionState::Disconnected; - let _ = ConnectionState::Connecting; - let _ = ConnectionState::Connected; - let _ = ConnectionState::Error; - } - - #[test] - fn deserializes_runtime_attachment_variants() { - let attachments: Vec = serde_json::from_value(json!([ - { - "type": "file", - "path": "/tmp/file.rs", - "displayName": "file.rs", - "lineRange": { "start": 7, "end": 12 } - }, - { - "type": "directory", - "path": "/tmp/project", - "displayName": "project" - }, - { - "type": "selection", - "filePath": "/tmp/lib.rs", - "displayName": "lib.rs", - "text": "fn main() {}", - "selection": { - "start": { "line": 1, "character": 2 }, - "end": { "line": 3, "character": 4 } - } - }, - { - "type": "blob", - "data": "Zm9v", - "mimeType": "image/png", - "displayName": "image.png" - }, - { - "type": "github_reference", - "number": 42, - "title": "Fix rendering", - "referenceType": "issue", - "state": "open", - "url": "https://github.com/example/repo/issues/42" - }, - { - "type": "extension_context", - "capturedAt": "2026-09-18T11:00:00Z", - "extensionId": "example:extension", - "title": "Unbound context" - } - ])) - .expect("attachments should deserialize"); - - assert_eq!(attachments.len(), 6); - assert!(matches!( - &attachments[0], - Attachment::File { - path, - display_name, - line_range: Some(AttachmentLineRange { start: 7, end: 12 }), - } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs") - )); - assert!(matches!( - &attachments[1], - Attachment::Directory { path, display_name } - if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project") - )); - assert!(matches!( - &attachments[2], - Attachment::Selection { - file_path, - display_name, - selection: - AttachmentSelectionRange { - start: AttachmentSelectionPosition { line: 1, character: 2 }, - end: AttachmentSelectionPosition { line: 3, character: 4 }, - }, - .. - } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs") - )); - assert!(matches!( - &attachments[3], - Attachment::Blob { - data, - mime_type, - display_name, - } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png") - )); - assert!(matches!( - &attachments[4], - Attachment::GitHubReference { - number: 42, - title, - reference_type: GitHubReferenceType::Issue, - state, - url, - } if title == "Fix rendering" - && state == "open" - && url == "https://github.com/example/repo/issues/42" - )); - assert!(matches!( - &attachments[5], - Attachment::ExtensionContext { - captured_at, - extension_id, - canvas_id: None, - instance_id: None, - title, - payload: None, - } if captured_at == "2026-09-18T11:00:00Z" - && extension_id == "example:extension" - && title == "Unbound context" - )); - assert_eq!( - serde_json::to_value(&attachments[5]).expect("serialize extension context"), - json!({ - "type": "extension_context", - "capturedAt": "2026-09-18T11:00:00Z", - "extensionId": "example:extension", - "title": "Unbound context" - }) - ); - } - - #[test] - fn ensures_display_names_for_variants_that_support_them() { - let mut attachments = vec![ - Attachment::File { - path: PathBuf::from("/tmp/file.rs"), - display_name: None, - line_range: None, - }, - Attachment::Selection { - file_path: PathBuf::from("/tmp/src/lib.rs"), - display_name: None, - text: "fn main() {}".to_string(), - selection: AttachmentSelectionRange { - start: AttachmentSelectionPosition { - line: 0, - character: 0, - }, - end: AttachmentSelectionPosition { - line: 0, - character: 10, - }, - }, - }, - Attachment::Blob { - data: "Zm9v".to_string(), - mime_type: "image/png".to_string(), - display_name: None, - }, - Attachment::GitHubReference { - number: 7, - title: "Track regressions".to_string(), - reference_type: GitHubReferenceType::Issue, - state: "open".to_string(), - url: "https://example.com/issues/7".to_string(), - }, - ]; - - ensure_attachment_display_names(&mut attachments); - - assert_eq!(attachments[0].display_name(), Some("file.rs")); - assert_eq!(attachments[1].display_name(), Some("lib.rs")); - assert_eq!(attachments[2].display_name(), Some("attachment")); - assert_eq!(attachments[3].display_name(), None); - assert_eq!( - attachments[3].label(), - Some("Track regressions".to_string()) - ); - } - - #[test] - fn github_anchored_attachment_variants_round_trip() { - let cases = vec![ - ( - "github_commit", - json!({ - "type": "github_commit", - "message": "Fix the thing", - "oid": "abc123", - "repo": { "id": 1, "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/commit/abc123" - }), - ), - ( - "github_release", - json!({ - "type": "github_release", - "name": "v1.2.3", - "repo": { "name": "repo", "owner": "octocat" }, - "tagName": "v1.2.3", - "url": "https://github.com/octocat/repo/releases/tag/v1.2.3" - }), - ), - ( - "github_actions_job", - json!({ - "type": "github_actions_job", - "conclusion": "failure", - "jobId": 99, - "jobName": "build", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/actions/runs/1/job/99", - "workflowName": "CI" - }), - ), - ( - "github_repository", - json!({ - "type": "github_repository", - "description": "An example repository", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo" - }), - ), - ( - "github_file_diff", - json!({ - "type": "github_file_diff", - "base": { - "path": "src/lib.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" } - }, - "head": { - "path": "src/lib.rs", - "ref": "feature", - "repo": { "name": "repo", "owner": "octocat" } - }, - "url": "https://github.com/octocat/repo/compare/main...feature" - }), - ), - ( - "github_tree_comparison", - json!({ - "type": "github_tree_comparison", - "base": { - "repo": { "name": "repo", "owner": "octocat" }, - "revision": "main" - }, - "head": { - "repo": { "name": "repo", "owner": "octocat" }, - "revision": "feature" - }, - "url": "https://github.com/octocat/repo/compare/main...feature" - }), - ), - ( - "github_url", - json!({ - "type": "github_url", - "url": "https://github.com/octocat/repo/wiki" - }), - ), - ( - "github_file", - json!({ - "type": "github_file", - "path": "src/main.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/blob/main/src/main.rs" - }), - ), - ( - "github_snippet", - json!({ - "type": "github_snippet", - "lineRange": { "start": 10, "end": 20 }, - "path": "src/main.rs", - "ref": "main", - "repo": { "name": "repo", "owner": "octocat" }, - "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20" - }), - ), - ]; - - for (expected_type, input) in cases { - let attachment: Attachment = serde_json::from_value(input.clone()) - .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}")); - - // Serialize to a string first: parsing into `serde_json::Value` would - // silently dedupe a duplicate `type` key, hiding the exact regression - // this test guards against (e.g. a wrapped generated struct emitting its - // own `type` alongside the enum tag). - let serialized_string = serde_json::to_string(&attachment) - .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}")); - - // Exactly one `type` key, carrying the expected discriminator. - assert_eq!( - serialized_string.matches("\"type\":").count(), - 1, - "{expected_type} must serialize a single `type` key" - ); - - let serialized: serde_json::Value = serde_json::from_str(&serialized_string) - .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}")); - assert_eq!( - serialized.get("type").and_then(|value| value.as_str()), - Some(expected_type), - "{expected_type} must serialize the correct discriminator" - ); - - // Round-trips without dropping fields. - assert_eq!( - serialized, input, - "{expected_type} should round-trip without data loss" - ); - let reparsed: Attachment = serde_json::from_value(serialized) - .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}")); - assert_eq!( - reparsed, attachment, - "{expected_type} should re-deserialize to the same value" - ); - } - } -} +mod tests; #[cfg(test)] -mod permission_builder_tests { - use std::sync::Arc; - - use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; - use crate::permission; - use crate::types::{ - PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, - SessionId, - }; - - fn data() -> PermissionRequestData { - PermissionRequestData { - extra: serde_json::json!({"tool": "shell"}), - ..Default::default() - } - } - - /// Apply the same policy-resolution logic that `Client::create_session` - /// uses, so tests exercise the effective handler. - fn resolve_create(mut cfg: SessionConfig) -> Option> { - permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) - } - - fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option> { - permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) - } - - async fn dispatch(handler: &Arc) -> PermissionResult { - handler - .handle(SessionId::from("s1"), RequestId::new("1"), data()) - .await - } - - #[tokio::test] - async fn approve_all_with_handler_present_approves() { - let cfg = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let h = resolve_create(cfg).expect("policy + handler yields handler"); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_all_standalone_produces_handler() { - let cfg = SessionConfig::default().approve_all_permissions(); - let h = resolve_create(cfg).expect("policy alone yields handler"); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - /// Phase I: order between with_permission_handler and the policy - /// builder must not matter. - #[tokio::test] - async fn approve_all_is_order_independent() { - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let b = SessionConfig::default() - .approve_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn deny_all_is_order_independent() { - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .deny_all_permissions(); - let b = SessionConfig::default() - .deny_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_permissions_if_consults_predicate() { - let cfg = SessionConfig::default().approve_permissions_if(|d| { - d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }); - let h = resolve_create(cfg).unwrap(); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn approve_permissions_if_is_order_independent() { - let predicate = |d: &PermissionRequestData| { - d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }; - let a = SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_permissions_if(predicate); - let b = SessionConfig::default() - .approve_permissions_if(predicate) - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_create(a).unwrap(); - let hb = resolve_create(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::Reject(_), - .. - } - )); - } - - #[tokio::test] - async fn resume_session_config_approve_all_works() { - let cfg = ResumeSessionConfig::new(SessionId::from("s1")) - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let h = resolve_resume(cfg).unwrap(); - assert!(matches!( - dispatch(&h).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[tokio::test] - async fn resume_session_config_approve_all_is_order_independent() { - let a = ResumeSessionConfig::new(SessionId::from("s1")) - .with_permission_handler(Arc::new(ApproveAllHandler)) - .approve_all_permissions(); - let b = ResumeSessionConfig::new(SessionId::from("s1")) - .approve_all_permissions() - .with_permission_handler(Arc::new(ApproveAllHandler)); - let ha = resolve_resume(a).unwrap(); - let hb = resolve_resume(b).unwrap(); - assert!(matches!( - dispatch(&ha).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - assert!(matches!( - dispatch(&hb).await, - PermissionResult::Decision { - decision: PermissionDecision::ApproveOnce(_), - .. - } - )); - } - - #[test] - fn session_config_enable_experimental_mode_serializes_when_set() { - let cfg = SessionConfig::default().with_enable_experimental_mode(false); - assert_eq!(cfg.enable_experimental_mode, Some(false)); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("experimental-mode"))) - .expect("enable_experimental_mode config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, Some(false)); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); - } - - #[test] - fn session_config_enable_experimental_mode_omitted_when_none() { - let cfg = SessionConfig::default(); - assert_eq!(cfg.enable_experimental_mode, None); - - let (wire, _runtime) = cfg - .into_wire(Some(SessionId::from("no-experimental-mode"))) - .expect("default config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, None); - - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("isExperimentalMode").is_none()); - } - - #[test] - fn resume_session_config_enable_experimental_mode_serializes_when_set() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) - .with_enable_experimental_mode(false); - assert_eq!(cfg.enable_experimental_mode, Some(false)); - - let (wire, _runtime) = cfg - .into_wire() - .expect("resume enable_experimental_mode config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, Some(false)); - - let json = serde_json::to_value(&wire).unwrap(); - assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); - } - - #[test] - fn resume_session_config_enable_experimental_mode_omitted_when_none() { - let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); - assert_eq!(cfg.enable_experimental_mode, None); - - let (wire, _runtime) = cfg - .into_wire() - .expect("default resume config has no duplicate handlers"); - assert_eq!(wire.is_experimental_mode, None); - - let json = serde_json::to_value(&wire).unwrap(); - assert!(json.get("isExperimentalMode").is_none()); - } -} +mod permission_builder_tests; #[cfg(test)] -mod is_terminal_tests { - use super::Tool; - - #[test] - fn is_terminal_serializes_as_camel_case_when_set() { - let tool = Tool { - name: "clear_context".to_owned(), - is_terminal: true, - ..Default::default() - }; - let value = serde_json::to_value(&tool).expect("tool serializes"); - assert_eq!( - value.get("isTerminal"), - Some(&serde_json::Value::Bool(true)) - ); - } - - #[test] - fn is_terminal_is_omitted_when_false() { - let tool = Tool { - name: "plain".to_owned(), - ..Default::default() - }; - let value = serde_json::to_value(&tool).expect("tool serializes"); - assert!(value.get("isTerminal").is_none()); - } - - /// `Tool` has a hand-written `Debug` impl, so a new field is only reported - /// if it is added there by hand. Guard against that drift. - #[test] - fn is_terminal_appears_in_debug_output() { - let terminal = Tool { - name: "clear_context".to_owned(), - is_terminal: true, - ..Default::default() - }; - assert!(format!("{terminal:?}").contains("is_terminal: true")); - - let plain = Tool { - name: "plain".to_owned(), - ..Default::default() - }; - assert!(format!("{plain:?}").contains("is_terminal: false")); - } -} +mod is_terminal_tests; #[cfg(test)] mod refresh_custom_instructions_tests; diff --git a/rust/src/types/is_terminal_tests.rs b/rust/src/types/is_terminal_tests.rs new file mode 100644 index 0000000000..8e0090ab36 --- /dev/null +++ b/rust/src/types/is_terminal_tests.rs @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use super::Tool; + +#[test] +fn is_terminal_serializes_as_camel_case_when_set() { + let tool = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert_eq!( + value.get("isTerminal"), + Some(&serde_json::Value::Bool(true)) + ); +} + +#[test] +fn is_terminal_is_omitted_when_false() { + let tool = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert!(value.get("isTerminal").is_none()); +} + +/// `Tool` has a hand-written `Debug` impl, so a new field is only reported +/// if it is added there by hand. Guard against that drift. +#[test] +fn is_terminal_appears_in_debug_output() { + let terminal = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + assert!(format!("{terminal:?}").contains("is_terminal: true")); + + let plain = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + assert!(format!("{plain:?}").contains("is_terminal: false")); +} diff --git a/rust/src/types/permission_builder_tests.rs b/rust/src/types/permission_builder_tests.rs new file mode 100644 index 0000000000..c549f9d4d8 --- /dev/null +++ b/rust/src/types/permission_builder_tests.rs @@ -0,0 +1,259 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::Arc; + +use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; +use crate::permission; +use crate::types::{ + PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, + SessionId, +}; + +fn data() -> PermissionRequestData { + PermissionRequestData { + extra: serde_json::json!({"tool": "shell"}), + ..Default::default() + } +} + +/// Apply the same policy-resolution logic that `Client::create_session` +/// uses, so tests exercise the effective handler. +fn resolve_create(mut cfg: SessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) +} + +fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) +} + +async fn dispatch(handler: &Arc) -> PermissionResult { + handler + .handle(SessionId::from("s1"), RequestId::new("1"), data()) + .await +} + +#[tokio::test] +async fn approve_all_with_handler_present_approves() { + let cfg = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let h = resolve_create(cfg).expect("policy + handler yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_all_standalone_produces_handler() { + let cfg = SessionConfig::default().approve_all_permissions(); + let h = resolve_create(cfg).expect("policy alone yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +/// Phase I: order between with_permission_handler and the policy +/// builder must not matter. +#[tokio::test] +async fn approve_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = SessionConfig::default() + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn deny_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .deny_all_permissions(); + let b = SessionConfig::default() + .deny_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_permissions_if_consults_predicate() { + let cfg = SessionConfig::default() + .approve_permissions_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); + let h = resolve_create(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn approve_permissions_if_is_order_independent() { + let predicate = + |d: &PermissionRequestData| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell"); + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_permissions_if(predicate); + let b = SessionConfig::default() + .approve_permissions_if(predicate) + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::Reject(_), + .. + } + )); +} + +#[tokio::test] +async fn resume_session_config_approve_all_works() { + let cfg = ResumeSessionConfig::new(SessionId::from("s1")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let h = resolve_resume(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[tokio::test] +async fn resume_session_config_approve_all_is_order_independent() { + let a = ResumeSessionConfig::new(SessionId::from("s1")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = ResumeSessionConfig::new(SessionId::from("s1")) + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_resume(a).unwrap(); + let hb = resolve_resume(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision { + decision: PermissionDecision::ApproveOnce(_), + .. + } + )); +} + +#[test] +fn session_config_enable_experimental_mode_serializes_when_set() { + let cfg = SessionConfig::default().with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("experimental-mode"))) + .expect("enable_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); +} + +#[test] +fn session_config_enable_experimental_mode_omitted_when_none() { + let cfg = SessionConfig::default(); + assert_eq!(cfg.enable_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("no-experimental-mode"))) + .expect("default config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); +} + +#[test] +fn resume_session_config_enable_experimental_mode_serializes_when_set() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) + .with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume enable_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); +} + +#[test] +fn resume_session_config_enable_experimental_mode_omitted_when_none() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); + assert_eq!(cfg.enable_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); +} diff --git a/rust/src/types/tests.rs b/rust/src/types/tests.rs new file mode 100644 index 0000000000..a61c083a4f --- /dev/null +++ b/rust/src/types/tests.rs @@ -0,0 +1,1927 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::collections::HashMap; +use std::path::PathBuf; + +use serde_json::json; + +use super::{ + AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, + AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, ConnectionState, + CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry, ExpFlagValue, + ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, InfiniteSessionConfig, + LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, + NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, + ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, + SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, + ToolResultResponse, ensure_attachment_display_names, +}; +use crate::generated::session_events::TypedSessionEvent; + +#[test] +fn permission_response_capability_is_publicly_exported() { + assert_eq!( + serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(), + json!("interactive") + ); +} + +#[test] +fn tool_builder_composes() { + let tool = Tool::new("greet") + .with_description("Say hello") + .with_namespaced_name("hello/greet") + .with_instructions("Pass the user's name") + .with_parameters(json!({ + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] + })) + .with_overrides_built_in_tool(true) + .with_skip_permission(true); + assert_eq!(tool.name, "greet"); + assert_eq!(tool.description, "Say hello"); + assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet")); + assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name")); + assert_eq!(tool.parameters.get("type").unwrap(), &json!("object")); + assert!(tool.overrides_built_in_tool); + assert!(tool.skip_permission); +} + +#[test] +fn tool_defer_serialization() { + let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto); + assert_eq!(tool.defer, Some(super::DeferMode::Auto)); + let value = serde_json::to_value(&tool).unwrap(); + assert_eq!(value.get("defer").unwrap(), &json!("auto")); + + let plain = Tool::new("plain"); + let value = serde_json::to_value(&plain).unwrap(); + assert!(value.get("defer").is_none()); +} + +#[test] +fn tool_metadata_serialization() { + use indexmap::IndexMap; + + let mut metadata = IndexMap::new(); + metadata.insert( + "github.com/copilot:safeForTelemetry".to_string(), + json!({ "name": true, "inputsNames": false }), + ); + let tool = Tool::new("lookup").with_metadata(metadata); + let value = serde_json::to_value(&tool).unwrap(); + assert_eq!( + value + .get("metadata") + .unwrap() + .get("github.com/copilot:safeForTelemetry") + .unwrap(), + &json!({ "name": true, "inputsNames": false }) + ); + + // Empty metadata is omitted on the wire. + let plain = Tool::new("plain"); + let value = serde_json::to_value(&plain).unwrap(); + assert!(value.get("metadata").is_none()); +} + +#[test] +fn custom_agent_config_builder_with_model() { + let agent = CustomAgentConfig::new("my-agent", "You are helpful.") + .with_model("claude-haiku-4.5") + .with_display_name("My Agent"); + assert_eq!(agent.name, "my-agent"); + assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5")); + assert_eq!(agent.display_name.as_deref(), Some("My Agent")); +} + +#[test] +fn custom_agent_config_serializes_model() { + let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["model"], "claude-haiku-4.5"); + assert_eq!(wire["name"], "model-agent"); +} + +#[test] +fn custom_agent_config_omits_model_when_none() { + let agent = CustomAgentConfig::new("no-model-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("model").is_none()); +} + +#[test] +fn custom_agent_config_builder_with_reasoning_effort() { + let agent = CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + assert_eq!(agent.reasoning_effort.as_deref(), Some("high")); +} + +#[test] +fn custom_agent_config_serializes_reasoning_effort() { + let agent = CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high"); + let wire = serde_json::to_value(&agent).unwrap(); + assert_eq!(wire["reasoningEffort"], "high"); +} + +#[test] +fn custom_agent_config_omits_reasoning_effort_when_none() { + let agent = CustomAgentConfig::new("default-agent", "prompt"); + let wire = serde_json::to_value(&agent).unwrap(); + assert!(wire.get("reasoningEffort").is_none()); +} + +#[test] +#[should_panic(expected = "tool parameter schema must be a JSON object")] +fn tool_with_parameters_panics_on_non_object_value() { + let _ = Tool::new("noop").with_parameters(json!(null)); +} + +#[test] +fn tool_result_expanded_serializes_binary_results_for_llm() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded { + text_result_for_llm: "rendered chart".to_string(), + result_type: "success".to_string(), + binary_results_for_llm: Some(vec![ToolBinaryResult { + data: "aW1n".to_string(), + mime_type: "image/png".to_string(), + r#type: "image".to_string(), + description: Some("chart preview".to_string()), + }]), + session_log: None, + error: None, + tool_telemetry: None, + tool_references: None, + }), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!( + wire, + json!({ + "result": { + "textResultForLlm": "rendered chart", + "resultType": "success", + "binaryResultsForLlm": [ + { + "data": "aW1n", + "mimeType": "image/png", + "type": "image", + "description": "chart preview" + } + ] + } + }) + ); +} + +#[test] +fn tool_result_expanded_omits_binary_results_for_llm_when_none() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded { + text_result_for_llm: "ok".to_string(), + result_type: "success".to_string(), + binary_results_for_llm: None, + session_log: None, + error: None, + tool_telemetry: None, + tool_references: None, + }), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!(wire["result"]["textResultForLlm"], "ok"); + assert!(wire["result"].get("binaryResultsForLlm").is_none()); +} + +#[test] +fn tool_result_expanded_serializes_tool_references() { + let response = ToolResultResponse { + result: ToolResult::Expanded( + ToolResultExpanded::new("found 2 tools", "success") + .with_tool_references(["get_weather", "check_status"]), + ), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!( + wire, + json!({ + "result": { + "textResultForLlm": "found 2 tools", + "resultType": "success", + "toolReferences": ["get_weather", "check_status"] + } + }) + ); +} + +#[test] +fn tool_result_expanded_omits_tool_references_when_none() { + let response = ToolResultResponse { + result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")), + }; + + let wire = serde_json::to_value(&response).unwrap(); + + assert_eq!(wire["result"]["textResultForLlm"], "ok"); + assert!(wire["result"].get("toolReferences").is_none()); +} + +#[test] +fn tool_result_expanded_with_tool_references_accepts_owned_strings() { + // The builder is generic over `Into`, so an owned `Vec` + // must compile and populate the field just like a `&str` array. + let names: Vec = vec!["alpha".to_string(), "beta".to_string()]; + let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); +} + +#[test] +fn tool_result_expanded_deserializes_tool_references() { + let wire = json!({ + "textResultForLlm": "found tools", + "resultType": "success", + "toolReferences": ["alpha", "beta"] + }); + + let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap(); + + assert_eq!( + expanded.tool_references.as_deref(), + Some(["alpha".to_string(), "beta".to_string()].as_slice()) + ); +} + +#[test] +fn session_config_default_wire_flags_off_without_handlers() { + let cfg = SessionConfig::default(); + assert_eq!(cfg.mcp_oauth_token_storage, None); + assert_eq!(cfg.allowed_models, None); + // Wire flags are derived from handler presence at create_session + // time, not stored on the config. With no handlers installed, every + // request_* flag should serialize as false. + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("default-flags"))) + .expect("default config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); + assert!(json.get("allowedModels").is_none()); +} + +#[test] +fn resume_session_config_new_wire_flags_off_without_handlers() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags")); + assert_eq!(cfg.mcp_oauth_token_storage, None); + assert_eq!(cfg.allowed_models, None); + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); + assert!(json.get("allowedModels").is_none()); +} + +#[test] +fn session_configs_build_debug_and_serialize_allowed_models() { + let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]); + assert_eq!( + create.allowed_models.as_deref(), + Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..]) + ); + assert!(format!("{create:?}").contains("allowed_models")); + + let (create_wire, _) = create + .into_wire(Some(SessionId::from("create-allowed-models"))) + .expect("allowed model config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["allowedModels"], + json!(["gpt-5.4", "claude-sonnet-4"]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models")) + .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]); + assert_eq!( + resume.allowed_models.as_deref(), + Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..]) + ); + assert!(format!("{resume:?}").contains("allowed_models")); + + let (resume_wire, _) = resume + .into_wire() + .expect("resume allowed model config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["allowedModels"], + json!(["gpt-5.4", "gpt-5-mini"]) + ); +} + +#[test] +fn custom_agents_local_only_serializes_on_create_and_resume() { + let (create_wire, _) = SessionConfig::default() + .with_custom_agents_local_only(false) + .into_wire(Some(SessionId::from("create-locality"))) + .expect("create config has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["customAgentsLocalOnly"], false); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality")) + .with_custom_agents_local_only(false) + .into_wire() + .expect("resume config has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["customAgentsLocalOnly"], false); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("create-unset"))) + .expect("create config has no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("customAgentsLocalOnly").is_none()); + + let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset")) + .into_wire() + .expect("resume config has no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("customAgentsLocalOnly").is_none()); +} + +#[test] +fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = SessionConfig::default().with_enable_mcp_apps(true); + assert_eq!(cfg.enable_mcp_apps, Some(true)); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("enable-mcp-apps"))) + .expect("enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); +} + +#[test] +fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps")) + .with_enable_mcp_apps(true); + assert_eq!(cfg.enable_mcp_apps, Some(true)); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); +} + +#[test] +fn github_mcp_tool_config_serializes_for_create_and_resume() { + let github_config = GitHubMcpToolConfig::new() + .with_enable_all_tools(true) + .with_additional_toolsets(["repos"]) + .with_additional_tools(["get_issue"]) + .with_enable_insiders_mode(true) + .with_disable_form_deferral(true); + + let (create_wire, _) = SessionConfig::default() + .with_github_mcp_tool_config(github_config.clone()) + .into_wire(Some(SessionId::from("github-mcp"))) + .expect("create config has no duplicate handlers"); + assert_eq!( + serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"], + serde_json::json!({ + "enableAllTools": true, + "additionalToolsets": ["repos"], + "additionalTools": ["get_issue"], + "enableInsidersMode": true, + "disableFormDeferral": true, + }) + ); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp")) + .with_github_mcp_tool_config(github_config) + .into_wire() + .expect("resume config has no duplicate handlers"); + assert!(resume_wire.github_mcp_tool_config.is_some()); + + let (unset_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("github-mcp-unset"))) + .expect("default config has no duplicate handlers"); + assert!( + serde_json::to_value(&unset_wire) + .unwrap() + .get("githubMcpToolConfig") + .is_none() + ); +} + +#[test] +fn memory_configuration_constructors_and_serde() { + assert!(MemoryConfiguration::enabled().enabled); + assert!(!MemoryConfiguration::disabled().enabled); + assert!(MemoryConfiguration::disabled().with_enabled(true).enabled); + + let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap(); + assert_eq!(json, serde_json::json!({ "enabled": true })); +} + +#[test] +fn session_config_with_memory_serializes() { + let (wire, _runtime) = SessionConfig::default() + .with_memory(MemoryConfiguration::enabled()) + .into_wire(Some(SessionId::from("memory-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); + + let (wire_off, _) = SessionConfig::default() + .with_memory(MemoryConfiguration::disabled()) + .into_wire(Some(SessionId::from("memory-off"))) + .expect("no duplicate handlers"); + let json_off = serde_json::to_value(&wire_off).unwrap(); + assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false })); + + // Unset memory is omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("memory-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("memory").is_none()); +} + +#[test] +fn resume_session_config_with_memory_serializes() { + let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on")) + .with_memory(MemoryConfiguration::enabled()) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["memory"], serde_json::json!({ "enabled": true })); + + // Unset memory is omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("memory").is_none()); +} + +#[test] +fn feature_flags_serialize_on_create_and_resume() { + let feature_flags = HashMap::from([ + ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), + ("DISABLED_TEST_FLAG".to_string(), false), + ]); + let expected = serde_json::json!({ + "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, + "DISABLED_TEST_FLAG": false, + }); + + let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); + assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); + let (create_wire, _) = create_config + .into_wire(Some(SessionId::from("feature-flags-create"))) + .expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["featureFlags"], expected); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) + .with_feature_flags(feature_flags) + .into_wire() + .expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["featureFlags"], expected); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("feature-flags-create-unset"))) + .expect("no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("featureFlags").is_none()); + + let (unset_resume_wire, _) = + ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("featureFlags").is_none()); +} + +fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { + CopilotExpAssignmentResponse { + features: vec!["copilot_exp_flag".to_string()], + flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]), + configs: vec![ExpConfigEntry { + id: "cfg-1".to_string(), + parameters: HashMap::from([ + ("threshold".to_string(), ExpFlagValue::Integer(5)), + ("enabled".to_string(), ExpFlagValue::Bool(true)), + ]), + }], + assignment_context: context.to_string(), + ..Default::default() + } +} + +#[test] +fn exp_flag_value_round_trips_all_variants() { + let values = serde_json::json!({ + "s": "text", + "i": 7, + "f": 1.5, + "b": true, + "n": null, + }); + let parsed: HashMap = serde_json::from_value(values.clone()).unwrap(); + assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string())); + assert_eq!(parsed["i"], ExpFlagValue::Integer(7)); + assert_eq!(parsed["f"], ExpFlagValue::Float(1.5)); + assert_eq!(parsed["b"], ExpFlagValue::Bool(true)); + assert_eq!(parsed["n"], ExpFlagValue::Null); + assert_eq!(serde_json::to_value(&parsed).unwrap(), values); +} + +#[test] +fn session_config_with_exp_assignments_serializes() { + let assignments = sample_exp_assignments("ctx-123"); + let expected = serde_json::to_value(&assignments).unwrap(); + let (wire, _runtime) = SessionConfig::default() + .with_exp_assignments(assignments) + .into_wire(Some(SessionId::from("exp-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["expAssignments"], expected); + assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123"); + assert_eq!( + json["expAssignments"]["Flights"]["copilot_exp_flag"], + "treatment" + ); + + // Unset exp assignments are omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("exp-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("expAssignments").is_none()); +} + +#[test] +fn resume_session_config_with_exp_assignments_serializes() { + let assignments = sample_exp_assignments("ctx-456"); + let expected = serde_json::to_value(&assignments).unwrap(); + let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on")) + .with_exp_assignments(assignments) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["expAssignments"], expected); + + // Unset exp assignments are omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("expAssignments").is_none()); +} + +#[test] +fn session_config_clone_preserves_exp_assignments() { + let assignments = sample_exp_assignments("ctx-clone"); + let config = SessionConfig::default().with_exp_assignments(assignments.clone()); + let cloned = config.clone(); + + assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); + + let (wire, _runtime) = cloned + .into_wire(Some(SessionId::from("exp-clone"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["expAssignments"], + serde_json::to_value(&assignments).unwrap() + ); +} + +#[test] +fn resume_session_config_clone_preserves_exp_assignments() { + let assignments = sample_exp_assignments("ctx-clone-resume"); + let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone")) + .with_exp_assignments(assignments.clone()); + let cloned = config.clone(); + + assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments)); + + let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["expAssignments"], + serde_json::to_value(&assignments).unwrap() + ); +} + +#[test] +#[allow(clippy::field_reassign_with_default)] +fn session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + use super::{CloudSessionOptions, CloudSessionRepository}; + + let mut cfg = SessionConfig::default(); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(false); + cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Concise); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); + cfg.enable_on_demand_instruction_discovery = Some(false); + cfg.cloud = Some(CloudSessionOptions::with_repository( + CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), + )); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("custom-id"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "custom-id"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "concise"); + assert_eq!(wire_json["remoteSession"], "export"); + assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); + assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); + assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); + assert_eq!(wire_json["cloud"]["repository"]["branch"], "main"); + + // Unset fields are omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("gitHubToken").is_none()); + assert!(empty_json.get("enableSessionTelemetry").is_none()); + assert!(empty_json.get("reasoningSummary").is_none()); + assert!(empty_json.get("remoteSession").is_none()); + assert!( + empty_json + .get("enableOnDemandInstructionDiscovery") + .is_none() + ); + assert!(empty_json.get("cloud").is_none()); +} + +#[test] +fn session_config_into_wire_serializes_named_providers_and_models() { + let cfg = SessionConfig::default() + .with_providers(vec![ + NamedProviderConfig::new("my-openai", "https://api.example.com/v1") + .with_provider_type("openai") + .with_wire_api("responses") + .with_model_provider("ollama") + .with_api_key("sk-test"), + ]) + .with_models(vec![ + ProviderModelConfig::new("gpt-x", "my-openai") + .with_wire_model("gpt-x-2025") + .with_max_output_tokens(2048), + ]); + + let (wire, _) = cfg + .into_wire(Some(SessionId::from("sess-providers"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["providers"][0]["name"], "my-openai"); + assert_eq!( + wire_json["providers"][0]["baseUrl"], + "https://api.example.com/v1" + ); + assert_eq!(wire_json["providers"][0]["type"], "openai"); + assert_eq!(wire_json["providers"][0]["wireApi"], "responses"); + assert_eq!(wire_json["providers"][0]["modelProvider"], "ollama"); + assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test"); + assert_eq!(wire_json["models"][0]["id"], "gpt-x"); + assert_eq!(wire_json["models"][0]["provider"], "my-openai"); + assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025"); + assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("providers").is_none()); + assert!(empty_json.get("models").is_none()); +} + +#[test] +fn resume_config_into_wire_serializes_named_providers_and_models() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume")) + .with_providers(vec![ + NamedProviderConfig::new("my-azure", "https://example.openai.azure.com") + .with_provider_type("azure") + .with_azure(AzureProviderOptions { + api_version: Some("2024-10-21".to_string()), + }), + ]) + .with_models(vec![ + ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"), + ]); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["providers"][0]["name"], "my-azure"); + assert_eq!(wire_json["providers"][0]["type"], "azure"); + assert_eq!( + wire_json["providers"][0]["azure"]["apiVersion"], + "2024-10-21" + ); + assert_eq!(wire_json["models"][0]["id"], "deploy-1"); + assert_eq!(wire_json["models"][0]["provider"], "my-azure"); + assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o"); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty")) + .into_wire() + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("providers").is_none()); + assert!(empty_json.get("models").is_none()); +} + +#[test] +fn session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let cfg = SessionConfig { + plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), + disabled_mcp_servers: Some(vec!["local-files".to_string(), "remote-github".to_string()]), + large_output: Some( + LargeToolOutputConfig::new() + .with_enabled(true) + .with_max_size_bytes(1024) + .with_output_directory(PathBuf::from("/tmp/large-output")), + ), + ..Default::default() + }; + + let (wire, _) = cfg + .into_wire(Some(SessionId::from("sess-1"))) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files", "remote-github"]) + ); + assert_eq!(wire_json["largeOutput"]["enabled"], true); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("empty"))) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); + assert!(empty_json.get("largeOutput").is_none()); +} + +#[test] +fn resume_session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.config_directory = Some(PathBuf::from("/tmp/cfg")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(true); + cfg.enable_session_telemetry = Some(false); + cfg.reasoning_summary = Some(ReasoningSummary::Detailed); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); + cfg.enable_on_demand_instruction_discovery = Some(false); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "sess-1"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["reasoningSummary"], "detailed"); + assert_eq!(wire_json["remoteSession"], "on"); + assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false); + + // Unset remote_session is omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("reasoningSummary").is_none()); + assert!(empty_json.get("remoteSession").is_none()); + assert!( + empty_json + .get("enableOnDemandInstructionDiscovery") + .is_none() + ); +} + +#[test] +fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); + cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]); + cfg.large_output = Some( + LargeToolOutputConfig::new() + .with_enabled(false) + .with_max_size_bytes(2048) + .with_output_directory(PathBuf::from("/tmp/large-output-r")), + ); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files-r"]) + ); + assert_eq!(wire_json["largeOutput"]["enabled"], false); + assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); + assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); + assert!(empty_json.get("largeOutput").is_none()); +} + +#[test] +fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() { + let url = "https://example.com/oauth/client-metadata.json"; + + let (create_wire, _) = SessionConfig::default() + .with_auth_client_id_metadata_url(url) + .into_wire(None) + .expect("default create has no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["authClientIdMetadataUrl"], url); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_auth_client_id_metadata_url(url) + .into_wire() + .expect("default resume has no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["authClientIdMetadataUrl"], url); + + let (empty_create_wire, _) = SessionConfig::default() + .into_wire(None) + .expect("default create has no duplicate handlers"); + let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap(); + assert!(empty_create_json.get("authClientIdMetadataUrl").is_none()); + + let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap(); + assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none()); +} + +#[test] +fn session_config_clones_disabled_mcp_servers() { + let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); + let mut create_clone = create.clone(); + create_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + create.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_disabled_mcp_servers(["local-files"]); + let mut resume_clone = resume.clone(); + resume_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + resume.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); +} + +#[test] +fn session_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = SessionConfig::default() + .with_session_id(SessionId::from("sess-1")) + .with_model("claude-sonnet-4") + .with_client_name("test-app") + .with_reasoning_effort("medium") + .with_reasoning_summary(ReasoningSummary::Concise) + .with_context_tier("long_context") + .with_streaming(true) + .with_tools([Tool::new("greet")]) + .with_available_tools(["bash", "view"]) + .with_excluded_tools(["dangerous"]) + .with_mcp_servers(IndexMap::new()) + .with_mcp_oauth_token_storage("persistent") + .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(true) + .with_skill_directories([PathBuf::from("/tmp/skills")]) + .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) + .with_agent("researcher") + .with_config_directory(PathBuf::from("/tmp/config")) + .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) + .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .with_enable_session_telemetry(false) + .with_include_sub_agent_streaming_events(false) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); + + assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); + assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); + assert_eq!(cfg.client_name.as_deref(), Some("test-app")); + assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise)); + assert_eq!(cfg.context_tier.as_deref(), Some("long_context")); + assert_eq!(cfg.streaming, Some(true)); + assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); + assert_eq!( + cfg.available_tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert_eq!( + cfg.excluded_tools.as_deref(), + Some(&["dangerous".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); + assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true)); + assert_eq!( + cfg.skill_directories.as_deref(), + Some(&[PathBuf::from("/tmp/skills")][..]) + ); + assert_eq!( + cfg.disabled_skills.as_deref(), + Some(&["broken-skill".to_string()][..]) + ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + assert_eq!(cfg.agent.as_deref(), Some("researcher")); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); + assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + ); + assert_eq!(cfg.enable_session_telemetry, Some(false)); + assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); +} + +#[test] +fn resume_session_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")) + .with_client_name("test-app") + .with_reasoning_summary(ReasoningSummary::None) + .with_context_tier("default") + .with_streaming(true) + .with_tools([Tool::new("greet")]) + .with_available_tools(["bash", "view"]) + .with_excluded_tools(["dangerous"]) + .with_mcp_servers(IndexMap::new()) + .with_mcp_oauth_token_storage("persistent") + .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(false) + .with_skill_directories([PathBuf::from("/tmp/skills")]) + .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) + .with_agent("researcher") + .with_config_directory(PathBuf::from("/tmp/config")) + .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) + .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .with_enable_session_telemetry(false) + .with_include_sub_agent_streaming_events(true) + .with_suppress_resume_event(true) + .with_continue_pending_work(true) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); + + assert_eq!(cfg.session_id.as_str(), "sess-2"); + assert_eq!(cfg.client_name.as_deref(), Some("test-app")); + assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None)); + assert_eq!(cfg.context_tier.as_deref(), Some("default")); + assert_eq!(cfg.streaming, Some(true)); + assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1)); + assert_eq!( + cfg.available_tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert_eq!( + cfg.excluded_tools.as_deref(), + Some(&["dangerous".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent")); + assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false)); + assert_eq!( + cfg.skill_directories.as_deref(), + Some(&[PathBuf::from("/tmp/skills")][..]) + ); + assert_eq!( + cfg.disabled_skills.as_deref(), + Some(&["broken-skill".to_string()][..]) + ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + assert_eq!(cfg.agent.as_deref(), Some("researcher")); + assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); + assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); + assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + ); + assert_eq!(cfg.enable_session_telemetry, Some(false)); + assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); + assert_eq!(cfg.suppress_resume_event, Some(true)); + assert_eq!(cfg.continue_pending_work, Some(true)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); +} + +/// `continue_pending_work` must serialize to wire as `continuePendingWork` +/// — the runtime keys off this exact field name to opt into the +/// pending-work-handoff pattern. +#[test] +fn resume_session_config_serializes_continue_pending_work_to_camel_case() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["continuePendingWork"], true); + + // Unset case — skip_serializing_if must omit the field. + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("continuePendingWork").is_none()); +} + +#[test] +fn resume_policy_and_recovery_report_round_trip() { + let config = + ResumeSessionConfig::new(SessionId::from("sess-1")).with_allow_transcript_recovery(false); + let (wire, _) = config.into_wire().unwrap(); + let value = serde_json::to_value(&wire).unwrap(); + assert_eq!(value["allowTranscriptRecovery"], false); + + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .unwrap(); + assert!( + serde_json::to_value(&wire) + .unwrap() + .get("allowTranscriptRecovery") + .is_none() + ); + + let result: crate::types::ResumeSessionResult = serde_json::from_value(serde_json::json!({ + "sessionId": "sess-1", + "transcriptRecovery": { + "plannedBackupPath": "events.jsonl.backup", + "invalidLineNumbers": [2], + "sessionStartMoved": false + } + })) + .unwrap(); + let recovery = result.transcript_recovery.unwrap(); + assert_eq!(recovery.invalid_line_numbers, vec![2]); + assert_eq!(recovery.planned_backup_path, "events.jsonl.backup"); +} + +#[test] +fn session_configs_serialize_additional_directories() { + let create = SessionConfig::default().with_additional_directories([ + PathBuf::from("/tmp/shared"), + PathBuf::from("/tmp/generated"), + ]); + let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["additionalDirectories"], + serde_json::json!(["/tmp/shared", "/tmp/generated"]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_additional_directories([PathBuf::from("/tmp/resumed")]); + let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["additionalDirectories"], + serde_json::json!(["/tmp/resumed"]) + ); +} + +/// The Rust field is `suppress_resume_event`, but the wire field stays +/// `disableResume` to preserve compatibility with the runtime and other +/// SDKs. +#[test] +fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["disableResume"], true); + assert!(json.get("suppressResumeEvent").is_none()); +} + +/// `instruction_directories` must serialize to wire as +/// `instructionDirectories` on `SessionConfig`. +#[test] +fn session_config_serializes_instruction_directories_to_camel_case() { + let cfg = SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]); + let (wire, _) = cfg + .into_wire(Some(SessionId::from("instr-on"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["instructionDirectories"], + serde_json::json!(["/tmp/instr"]) + ); + + // Unset case — skip_serializing_if must omit the field. + let (wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("instr-off"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); +} + +/// Same check on the resume path. Forwarded to the CLI on +/// `session.resume`. +#[test] +fn resume_session_config_serializes_instruction_directories_to_camel_case() { + let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_instruction_directories([PathBuf::from("/tmp/instr")]); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["instructionDirectories"], + serde_json::json!(["/tmp/instr"]) + ); + + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); +} + +#[test] +fn custom_agent_config_builder_composes() { + use indexmap::IndexMap; + + let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.") + .with_display_name("Research Assistant") + .with_description("Investigates technical questions.") + .with_tools(["bash", "view"]) + .with_mcp_servers(IndexMap::new()) + .with_infer(true) + .with_skills(["rust-coding-skill"]); + + assert_eq!(cfg.name, "researcher"); + assert_eq!(cfg.prompt, "You are a research assistant."); + assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant")); + assert_eq!( + cfg.description.as_deref(), + Some("Investigates technical questions.") + ); + assert_eq!( + cfg.tools.as_deref(), + Some(&["bash".to_string(), "view".to_string()][..]) + ); + assert!(cfg.mcp_servers.is_some()); + assert_eq!(cfg.infer, Some(true)); + assert_eq!( + cfg.skills.as_deref(), + Some(&["rust-coding-skill".to_string()][..]) + ); +} + +#[test] +fn mcp_servers_serialize_in_insertion_order() { + use indexmap::IndexMap; + + // Regression: `mcp_servers` was a `HashMap`, so the server keys (and + // thus the `session.create` payload) serialized in a per-process + // random order; `IndexMap` pins them to insertion order. The long + // sequence makes a `HashMap` regression reproduce this exact order by + // chance only 1/N!, avoiding a flaky false pass. + let order = [ + "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon", "ceres", + "nova", "kelp", "otter", "yodel", "plum", "garnet", + ]; + let mut servers = IndexMap::new(); + for name in order { + servers.insert( + name.to_string(), + McpServerConfig::Stdio(McpStdioServerConfig { + command: "run".to_string(), + ..Default::default() + }), + ); + } + + let (wire, _runtime) = SessionConfig::default() + .with_mcp_servers(servers) + .into_wire(None) + .expect("into_wire should succeed"); + let json = serde_json::to_string(&wire).expect("serialize wire"); + + let positions: Vec = order + .iter() + .map(|name| { + json.find(&format!("\"{name}\"")) + .unwrap_or_else(|| panic!("server {name} missing from wire JSON")) + }) + .collect(); + let mut ascending = positions.clone(); + ascending.sort_unstable(); + assert_eq!( + positions, ascending, + "mcp server keys must serialize in insertion order: {json}" + ); +} + +#[test] +fn infinite_session_config_builder_composes() { + let cfg = InfiniteSessionConfig::new() + .with_enabled(true) + .with_background_compaction_threshold(0.75) + .with_buffer_exhaustion_threshold(0.92); + + assert_eq!(cfg.enabled, Some(true)); + assert_eq!(cfg.background_compaction_threshold, Some(0.75)); + assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92)); +} + +#[test] +fn provider_config_builder_composes() { + use std::collections::HashMap; + + let mut headers = HashMap::new(); + headers.insert("X-Custom".to_string(), "value".to_string()); + + let cfg = ProviderConfig::new("https://api.example.com") + .with_provider_type("openai") + .with_wire_api("completions") + .with_transport("websockets") + .with_model_provider("lm_studio") + .with_api_key("sk-test") + .with_bearer_token("bearer-test") + .with_headers(headers) + .with_model_id("gpt-4") + .with_wire_model("azure-gpt-4-deployment") + .with_max_prompt_tokens(8192) + .with_max_output_tokens(2048); + + assert_eq!(cfg.base_url, "https://api.example.com"); + assert_eq!(cfg.provider_type.as_deref(), Some("openai")); + assert_eq!(cfg.wire_api.as_deref(), Some("completions")); + assert_eq!(cfg.transport.as_deref(), Some("websockets")); + assert_eq!(cfg.model_provider.as_deref(), Some("lm_studio")); + assert_eq!(cfg.api_key.as_deref(), Some("sk-test")); + assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test")); + assert_eq!( + cfg.headers + .as_ref() + .and_then(|h| h.get("X-Custom")) + .map(String::as_str), + Some("value"), + ); + assert_eq!(cfg.model_id.as_deref(), Some("gpt-4")); + assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment")); + assert_eq!(cfg.max_prompt_tokens, Some(8192)); + assert_eq!(cfg.max_output_tokens, Some(2048)); + + // Wire-shape: camelCase, skip_serializing_if when unset. + let wire = serde_json::to_value(&cfg).unwrap(); + assert_eq!(wire["modelId"], "gpt-4"); + assert_eq!(wire["wireModel"], "azure-gpt-4-deployment"); + assert_eq!(wire["modelProvider"], "lm_studio"); + assert_eq!(wire["maxPromptTokens"], 8192); + assert_eq!(wire["maxOutputTokens"], 2048); + + let unset = ProviderConfig::new("https://api.example.com"); + let wire_unset = serde_json::to_value(&unset).unwrap(); + assert!(wire_unset.get("modelId").is_none()); + assert!(wire_unset.get("wireModel").is_none()); + assert!(wire_unset.get("modelProvider").is_none()); + assert!(wire_unset.get("maxPromptTokens").is_none()); + assert!(wire_unset.get("maxOutputTokens").is_none()); +} + +#[test] +fn capi_session_options_builder_composes_and_serializes() { + let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false); + + assert_eq!(cfg.enable_web_socket_responses, Some(false)); + + let wire = serde_json::to_value(&cfg).unwrap(); + assert_eq!( + wire, + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let unset = CapiSessionOptions::new(); + let wire_unset = serde_json::to_value(&unset).unwrap(); + assert!(wire_unset.get("enableWebSocketResponses").is_none()); + assert!(wire_unset.get("autoTier").is_none()); + assert_eq!(wire_unset, json!({})); +} + +#[test] +fn capi_auto_tier_canonical_values_round_trip_and_forward() { + for (tier, value) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + (AutoTier::Fast, "fast"), + ] { + let exported: crate::AutoTier = tier.clone(); + let capi = CapiSessionOptions::new().with_auto_tier(exported); + assert_eq!(capi.auto_tier, Some(tier)); + assert_eq!( + serde_json::to_value(&capi).unwrap(), + json!({"autoTier": value}) + ); + assert_eq!( + serde_json::from_value::(json!({"autoTier": value})).unwrap(), + capi + ); + + let capi = capi.with_enable_web_socket_responses(false); + let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); + let (create, _) = SessionConfig::default() + .with_model("auto") + .with_capi(capi.clone()) + .into_wire(Some(SessionId::from("capi-create"))) + .unwrap(); + assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); + + let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(capi) + .into_wire() + .unwrap(); + assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); + } +} + +#[test] +fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { + for value in ["balanced", "Balance", "unknown"] { + assert_eq!( + serde_json::from_value::(json!(value)).unwrap(), + AutoTier::Unknown + ); + } + let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); + assert_eq!(capi.auto_tier, None); +} + +#[test] +fn session_config_with_capi_serializes() { + let (wire, _) = SessionConfig::default() + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .into_wire(Some(SessionId::from("capi-create"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("capi-create-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); +} + +#[test] +fn resume_session_config_with_capi_serializes() { + let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "enableWebSocketResponses": false }) + ); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); +} + +#[test] +fn system_message_config_builder_composes() { + use std::collections::HashMap; + + let cfg = SystemMessageConfig::new() + .with_mode("replace") + .with_content("Custom system message.") + .with_sections(HashMap::new()); + + assert_eq!(cfg.mode.as_deref(), Some("replace")); + assert_eq!(cfg.content.as_deref(), Some("Custom system message.")); + assert!(cfg.sections.is_some()); +} + +#[test] +fn delivery_mode_serializes_to_kebab_case_strings() { + assert_eq!( + serde_json::to_string(&DeliveryMode::Enqueue).unwrap(), + "\"enqueue\"" + ); + assert_eq!( + serde_json::to_string(&DeliveryMode::Immediate).unwrap(), + "\"immediate\"" + ); + let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap(); + assert_eq!(parsed, DeliveryMode::Immediate); +} + +#[test] +fn agent_mode_serializes_to_kebab_case_strings() { + assert_eq!( + serde_json::to_string(&AgentMode::Interactive).unwrap(), + "\"interactive\"" + ); + assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\""); + assert_eq!( + serde_json::to_string(&AgentMode::Autopilot).unwrap(), + "\"autopilot\"" + ); + assert_eq!( + serde_json::to_string(&AgentMode::Shell).unwrap(), + "\"shell\"" + ); + let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap(); + assert_eq!(parsed, AgentMode::Plan); +} + +#[test] +fn connection_state_distinguishes_variants() { + // ConnectionState is now an internal type; verify we can construct + // and compare the variants used by the lifecycle code paths. + assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected); +} + +/// `agentId` is the sub-agent attribution field added in copilot-sdk +/// commit f8cf846 ("Derive session event envelopes from schema"). +/// Every other SDK (Node, Python, Go, .NET) carries it on the event +/// envelope; Rust must too or sub-agent events lose attribution at +/// the deserialization boundary. Cross-SDK parity test. +#[test] +fn session_event_round_trips_agent_id_on_envelope() { + let wire = json!({ + "id": "evt-1", + "timestamp": "2026-04-30T12:00:00Z", + "parentId": null, + "agentId": "sub-agent-42", + "type": "assistant.message", + "data": { "message": "hi" } + }); + + let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); + + // Round-trip preserves the field on the wire. + let roundtripped = serde_json::to_value(&event).unwrap(); + assert_eq!(roundtripped["agentId"], "sub-agent-42"); + + // Absent agentId remains absent (skip_serializing_if). + let main_agent_event: SessionEvent = serde_json::from_value(json!({ + "id": "evt-2", + "timestamp": "2026-04-30T12:00:01Z", + "parentId": null, + "type": "session.idle", + "data": {} + })) + .unwrap(); + assert!(main_agent_event.agent_id.is_none()); + let roundtripped = serde_json::to_value(&main_agent_event).unwrap(); + assert!(roundtripped.get("agentId").is_none()); +} + +/// Same parity for the typed event envelope produced by the codegen. +#[test] +fn typed_session_event_round_trips_agent_id_on_envelope() { + let wire = json!({ + "id": "evt-1", + "timestamp": "2026-04-30T12:00:00Z", + "parentId": null, + "agentId": "sub-agent-42", + "type": "session.idle", + "data": {} + }); + + let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); + assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42")); + + let roundtripped = serde_json::to_value(&event).unwrap(); + assert_eq!(roundtripped["agentId"], "sub-agent-42"); +} + +#[test] +fn connection_state_variants_compile() { + // Defensive smoke test: all variants must be constructable from + // within the crate. (The enum was demoted from pub to pub(crate) + // in Phase D; this test guards against accidental removal.) + let _ = ConnectionState::Disconnected; + let _ = ConnectionState::Connecting; + let _ = ConnectionState::Connected; + let _ = ConnectionState::Error; +} + +#[test] +fn deserializes_runtime_attachment_variants() { + let attachments: Vec = serde_json::from_value(json!([ + { + "type": "file", + "path": "/tmp/file.rs", + "displayName": "file.rs", + "lineRange": { "start": 7, "end": 12 } + }, + { + "type": "directory", + "path": "/tmp/project", + "displayName": "project" + }, + { + "type": "selection", + "filePath": "/tmp/lib.rs", + "displayName": "lib.rs", + "text": "fn main() {}", + "selection": { + "start": { "line": 1, "character": 2 }, + "end": { "line": 3, "character": 4 } + } + }, + { + "type": "blob", + "data": "Zm9v", + "mimeType": "image/png", + "displayName": "image.png" + }, + { + "type": "github_reference", + "number": 42, + "title": "Fix rendering", + "referenceType": "issue", + "state": "open", + "url": "https://github.com/example/repo/issues/42" + }, + { + "type": "extension_context", + "capturedAt": "2026-09-18T11:00:00Z", + "extensionId": "example:extension", + "title": "Unbound context" + } + ])) + .expect("attachments should deserialize"); + + assert_eq!(attachments.len(), 6); + assert!(matches!( + &attachments[0], + Attachment::File { + path, + display_name, + line_range: Some(AttachmentLineRange { start: 7, end: 12 }), + } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs") + )); + assert!(matches!( + &attachments[1], + Attachment::Directory { path, display_name } + if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project") + )); + assert!(matches!( + &attachments[2], + Attachment::Selection { + file_path, + display_name, + selection: + AttachmentSelectionRange { + start: AttachmentSelectionPosition { line: 1, character: 2 }, + end: AttachmentSelectionPosition { line: 3, character: 4 }, + }, + .. + } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs") + )); + assert!(matches!( + &attachments[3], + Attachment::Blob { + data, + mime_type, + display_name, + } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png") + )); + assert!(matches!( + &attachments[4], + Attachment::GitHubReference { + number: 42, + title, + reference_type: GitHubReferenceType::Issue, + state, + url, + } if title == "Fix rendering" + && state == "open" + && url == "https://github.com/example/repo/issues/42" + )); + assert!(matches!( + &attachments[5], + Attachment::ExtensionContext { + captured_at, + extension_id, + canvas_id: None, + instance_id: None, + title, + payload: None, + } if captured_at == "2026-09-18T11:00:00Z" + && extension_id == "example:extension" + && title == "Unbound context" + )); + assert_eq!( + serde_json::to_value(&attachments[5]).expect("serialize extension context"), + json!({ + "type": "extension_context", + "capturedAt": "2026-09-18T11:00:00Z", + "extensionId": "example:extension", + "title": "Unbound context" + }) + ); +} + +#[test] +fn ensures_display_names_for_variants_that_support_them() { + let mut attachments = vec![ + Attachment::File { + path: PathBuf::from("/tmp/file.rs"), + display_name: None, + line_range: None, + }, + Attachment::Selection { + file_path: PathBuf::from("/tmp/src/lib.rs"), + display_name: None, + text: "fn main() {}".to_string(), + selection: AttachmentSelectionRange { + start: AttachmentSelectionPosition { + line: 0, + character: 0, + }, + end: AttachmentSelectionPosition { + line: 0, + character: 10, + }, + }, + }, + Attachment::Blob { + data: "Zm9v".to_string(), + mime_type: "image/png".to_string(), + display_name: None, + }, + Attachment::GitHubReference { + number: 7, + title: "Track regressions".to_string(), + reference_type: GitHubReferenceType::Issue, + state: "open".to_string(), + url: "https://example.com/issues/7".to_string(), + }, + ]; + + ensure_attachment_display_names(&mut attachments); + + assert_eq!(attachments[0].display_name(), Some("file.rs")); + assert_eq!(attachments[1].display_name(), Some("lib.rs")); + assert_eq!(attachments[2].display_name(), Some("attachment")); + assert_eq!(attachments[3].display_name(), None); + assert_eq!( + attachments[3].label(), + Some("Track regressions".to_string()) + ); +} + +#[test] +fn github_anchored_attachment_variants_round_trip() { + let cases = vec![ + ( + "github_commit", + json!({ + "type": "github_commit", + "message": "Fix the thing", + "oid": "abc123", + "repo": { "id": 1, "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/commit/abc123" + }), + ), + ( + "github_release", + json!({ + "type": "github_release", + "name": "v1.2.3", + "repo": { "name": "repo", "owner": "octocat" }, + "tagName": "v1.2.3", + "url": "https://github.com/octocat/repo/releases/tag/v1.2.3" + }), + ), + ( + "github_actions_job", + json!({ + "type": "github_actions_job", + "conclusion": "failure", + "jobId": 99, + "jobName": "build", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/actions/runs/1/job/99", + "workflowName": "CI" + }), + ), + ( + "github_repository", + json!({ + "type": "github_repository", + "description": "An example repository", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo" + }), + ), + ( + "github_file_diff", + json!({ + "type": "github_file_diff", + "base": { + "path": "src/lib.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" } + }, + "head": { + "path": "src/lib.rs", + "ref": "feature", + "repo": { "name": "repo", "owner": "octocat" } + }, + "url": "https://github.com/octocat/repo/compare/main...feature" + }), + ), + ( + "github_tree_comparison", + json!({ + "type": "github_tree_comparison", + "base": { + "repo": { "name": "repo", "owner": "octocat" }, + "revision": "main" + }, + "head": { + "repo": { "name": "repo", "owner": "octocat" }, + "revision": "feature" + }, + "url": "https://github.com/octocat/repo/compare/main...feature" + }), + ), + ( + "github_url", + json!({ + "type": "github_url", + "url": "https://github.com/octocat/repo/wiki" + }), + ), + ( + "github_file", + json!({ + "type": "github_file", + "path": "src/main.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/blob/main/src/main.rs" + }), + ), + ( + "github_snippet", + json!({ + "type": "github_snippet", + "lineRange": { "start": 10, "end": 20 }, + "path": "src/main.rs", + "ref": "main", + "repo": { "name": "repo", "owner": "octocat" }, + "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20" + }), + ), + ]; + + for (expected_type, input) in cases { + let attachment: Attachment = serde_json::from_value(input.clone()) + .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}")); + + // Serialize to a string first: parsing into `serde_json::Value` would + // silently dedupe a duplicate `type` key, hiding the exact regression + // this test guards against (e.g. a wrapped generated struct emitting its + // own `type` alongside the enum tag). + let serialized_string = serde_json::to_string(&attachment) + .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}")); + + // Exactly one `type` key, carrying the expected discriminator. + assert_eq!( + serialized_string.matches("\"type\":").count(), + 1, + "{expected_type} must serialize a single `type` key" + ); + + let serialized: serde_json::Value = serde_json::from_str(&serialized_string) + .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}")); + assert_eq!( + serialized.get("type").and_then(|value| value.as_str()), + Some(expected_type), + "{expected_type} must serialize the correct discriminator" + ); + + // Round-trips without dropping fields. + assert_eq!( + serialized, input, + "{expected_type} should round-trip without data loss" + ); + let reparsed: Attachment = serde_json::from_value(serialized) + .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}")); + assert_eq!( + reparsed, attachment, + "{expected_type} should re-deserialize to the same value" + ); + } +} diff --git a/rust/src/wire.rs b/rust/src/wire.rs index f5281476a5..35a6dcf9ee 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -123,6 +123,8 @@ pub(crate) struct SessionCreateWire { pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub has_skill_provider: Option, pub request_user_input: bool, pub request_permission: bool, pub request_exit_plan_mode: bool, @@ -291,6 +293,8 @@ pub(crate) struct SessionResumeWire { pub enable_session_store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub has_skill_provider: Option, pub request_user_input: bool, pub request_permission: bool, pub request_exit_plan_mode: bool, @@ -374,6 +378,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub allow_transcript_recovery: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub feature_flags: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index d4ea6cd7dd..d0b670f0af 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -4,15 +4,20 @@ #![allow(clippy::unwrap_used)] use github_copilot_sdk::rpc::{ - AcceptedEnqueueCommandResult, ConnectorAccountRequest, ConnectorCatalogStatus, - ConnectorConnectRequest, ConnectorContinueRequest, ConnectorReconcileRequest, + AcceptedEnqueueCommandResult, AuthIdentityMetadata, AuthInfoType, ConnectorAccountRequest, + ConnectorCapabilities, ConnectorCatalogEntry, ConnectorCatalogStatus, ConnectorConnectRequest, + ConnectorConnectResult, ConnectorContinueRequest, ConnectorReconcileOptions, + ConnectorReconcileRequest, ConnectorSessionAccount, ConnectorSessionAccountResult, + CustomizationReloadOutcome, CustomizationReloadStatus, CustomizationReloadSubsystem, EnqueueCommandResult, Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, - McpDisableRequest, McpEnableOptions, McpEnableRequest, McpInstallationOperationStatus, - McpOauthLoginOptions, McpOauthLoginRequest, McpServer, McpStopServerRequest, - ModelSetAllowedModelsRequest, ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, - ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, - SandboxConfig, SendAgentMode, TasksStartAgentRequest, UnsupportedEnqueueCommandResult, + McpConfiguredServer, McpConfiguredServerList, McpConfiguredServerState, McpDisableRequest, + McpEnableOptions, McpEnableRequest, McpInstallationOperationStatus, McpOauthLoginOptions, + McpOauthLoginRequest, McpServer, McpStopServerRequest, ModelSetAllowedModelsRequest, + ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, SandboxConfig, + SendAgentMode, SessionMcpListConfiguredResult, TasksStartAgentRequest, + UnsupportedEnqueueCommandResult, }; use github_copilot_sdk::session_events::{ McpServerStatus, PermissionRequest, PermissionRequestedData, SessionEventData, @@ -20,6 +25,91 @@ use github_copilot_sdk::session_events::{ }; use github_copilot_sdk::{AutoTier, AutoTierPreference, SetModelOptions}; +#[test] +fn configured_mcp_servers_preserve_enablement_without_live_state() { + for enabled in [false, true] { + let server = McpConfiguredServer { + name: "configured".to_string(), + enabled, + ..Default::default() + }; + let wire = serde_json::json!({ + "servers": [{ "name": "configured", "enabled": enabled }], + }); + assert_eq!( + serde_json::to_value(McpConfiguredServerList { + servers: vec![server], + }) + .unwrap(), + wire + ); + let result: SessionMcpListConfiguredResult = serde_json::from_value(wire.clone()).unwrap(); + assert!(result.servers[0].live.is_none()); + assert_eq!(serde_json::to_value(result).unwrap(), wire); + } +} + +#[test] +fn configured_mcp_servers_preserve_live_observations_independently_of_enablement() { + for enabled in [false, true] { + let server = McpConfiguredServer { + name: "configured".to_string(), + enabled, + live: Some(McpConfiguredServerState { + status: McpServerStatus::Stopped, + error: None, + }), + ..Default::default() + }; + assert_eq!( + serde_json::to_value(server).unwrap(), + serde_json::json!({ + "name": "configured", + "enabled": enabled, + "live": { "status": "stopped" }, + }) + ); + } + let wire = serde_json::json!({ + "servers": [{ + "name": "configured", + "enabled": true, + "live": { "status": "failed", "error": "connection failed" }, + }], + }); + let result: SessionMcpListConfiguredResult = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(result).unwrap(), wire); +} + +#[test] +fn customization_reload_outcome_preserves_future_wire_values() { + let wire = serde_json::json!({ + "status": "future-status", + "subsystem": "future-subsystem", + "detail": "new component", + }); + let outcome: CustomizationReloadOutcome = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(outcome).unwrap(), wire); + + assert_eq!( + serde_json::from_value::(serde_json::json!("reloaded")).unwrap(), + CustomizationReloadStatus::Reloaded + ); + assert_eq!( + serde_json::from_value::(serde_json::json!("skills")) + .unwrap(), + CustomizationReloadSubsystem::Skills + ); + assert_eq!( + serde_json::to_value(CustomizationReloadStatus::Reloaded).unwrap(), + serde_json::json!("reloaded") + ); + assert_eq!( + serde_json::to_value(CustomizationReloadSubsystem::Skills).unwrap(), + serde_json::json!("skills") + ); +} + #[test] fn operation_status_preserves_required_phases_and_original_identity() { for phase in [ @@ -363,6 +453,99 @@ fn connector_request_dtos_use_public_camel_case_wire_fields() { "refreshCatalog": true, }) ); + + let targeted = ConnectorReconcileOptions::new("account-1").force_connector_name("github"); + assert_eq!( + serde_json::to_value(targeted).unwrap(), + serde_json::json!({ + "accountId": "account-1", + "forceConnectorName": "github", + }) + ); +} + +#[test] +fn connector_session_account_is_nullable_and_credential_free() { + let account = ConnectorSessionAccount { + account_id: "session-account-1".to_string(), + auth_info: AuthIdentityMetadata { + r#type: AuthInfoType::Token, + host: "github.com".to_string(), + login: "alice".to_string(), + }, + }; + let expected = serde_json::json!({ + "accountId": "session-account-1", + "authInfo": { "type": "token", "host": "github.com", "login": "alice" }, + }); + assert_eq!(serde_json::to_value(&account).unwrap(), expected); + let decoded: ConnectorSessionAccountResult = serde_json::from_value(expected).unwrap(); + assert_eq!(decoded.unwrap().account_id, account.account_id); + let unavailable: ConnectorSessionAccountResult = + serde_json::from_value(serde_json::Value::Null).unwrap(); + assert!(unavailable.is_none()); + assert_eq!( + serde_json::to_value(unavailable).unwrap(), + serde_json::Value::Null + ); +} + +#[test] +fn connector_optional_capabilities_are_not_enabled_by_older_runtimes() { + for flag in [None, Some(false), Some(true)] { + let mut value = serde_json::json!({ + "apiVersion": 1, + "availability": "enabled", + "consentContinuation": true, + "opaqueAccountSelection": true, + "maxPollAttempts": 10, + "maxPollIntervalMs": 5_000, + "maxDeadlineMs": 60_000, + }); + if let Some(flag) = flag { + value["sessionAccountSelection"] = serde_json::json!(flag); + value["targetedReconcile"] = serde_json::json!(flag); + } + let capabilities: ConnectorCapabilities = serde_json::from_value(value).unwrap(); + assert_eq!(capabilities.session_account_selection, flag); + assert_eq!(capabilities.targeted_reconcile, flag); + assert_eq!( + capabilities.session_account_selection == Some(true), + flag == Some(true) + ); + assert_eq!( + capabilities.targeted_reconcile == Some(true), + flag == Some(true) + ); + } +} + +#[test] +fn connector_catalog_presentation_metadata_is_optional() { + let legacy = serde_json::json!({ + "name": "github", + "displayName": "GitHub", + "status": "connected", + "runtimeServerIds": ["connector-github"], + }); + let entry: ConnectorCatalogEntry = serde_json::from_value(legacy.clone()).unwrap(); + assert!(entry.logo.is_none()); + assert!(entry.tier.is_none()); + assert!(entry.release_tag.is_none()); + assert_eq!(serde_json::to_value(entry).unwrap(), legacy); + + let mut decorated = legacy; + decorated["logo"] = serde_json::json!("https://example.com/github.svg"); + decorated["tier"] = serde_json::json!("standard"); + decorated["releaseTag"] = serde_json::json!("preview"); + let entry: ConnectorCatalogEntry = serde_json::from_value(decorated.clone()).unwrap(); + assert_eq!( + entry.logo.as_deref(), + Some("https://example.com/github.svg") + ); + assert_eq!(entry.tier.as_deref(), Some("standard")); + assert_eq!(entry.release_tag.as_deref(), Some("preview")); + assert_eq!(serde_json::to_value(entry).unwrap(), decorated); } #[test] @@ -376,6 +559,23 @@ fn connector_catalog_unknown_wire_value_has_a_distinct_variant() { assert_eq!(future_status, ConnectorCatalogStatus::Unknown); } +#[test] +fn connector_connect_results_do_not_treat_unknown_outcomes_as_connected() { + let future_result = serde_json::json!({ + "kind": "future_outcome", + "continuationId": "continuation-1", + "consentUrl": "https://example.com/consent", + "status": { + "apiVersion": 1, + "availability": "enabled", + "runtimeServers": [], + "pendingConnections": 0 + } + }); + + assert!(serde_json::from_value::(future_result).is_err()); +} + #[test] fn model_allowed_models_request_and_result_preserve_contract_fields() { let replace = ModelSetAllowedModelsRequest { diff --git a/rust/tests/build_acquisition.rs b/rust/tests/build_acquisition.rs new file mode 100644 index 0000000000..8f0875a90b --- /dev/null +++ b/rust/tests/build_acquisition.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +#![cfg(test)] +#![allow(clippy::unwrap_used)] + +#[path = "../build/in_process.rs"] +mod implementation; diff --git a/rust/tests/cli_resolution_test.rs b/rust/tests/cli_resolution_test.rs index 97815a326e..aabe441ad9 100644 --- a/rust/tests/cli_resolution_test.rs +++ b/rust/tests/cli_resolution_test.rs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + //! Tests for the build-time and runtime CLI provisioning path. //! //! Covers the `COPILOT_CLI_PATH` env override, the build-time-extracted @@ -109,19 +111,19 @@ async fn stale_env_override_falls_through() { /// With `bundled-cli` off, `build.rs` extracts the runtime wrapper into the /// per-user cache and the runtime resolver recomputes its location from -/// `COPILOT_SDK_CLI_VERSION` + the OS-derived binary name. This test +/// `COPILOT_SDK_CLI_CACHE_ID` + the OS-derived binary name. This test /// mirrors that convention and asserts the file is on disk where the /// resolver expects to find it. #[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))] #[test] fn extracted_binary_present_at_conventional_path() { - let version = env!("COPILOT_SDK_CLI_VERSION"); + let cache_identity = env!("COPILOT_SDK_CLI_CACHE_ID"); let binary = if cfg!(windows) { "copilot-runtime.exe" } else { "copilot-runtime" }; - let sanitized = sanitize_version_for_test(version); + let sanitized = sanitize_cache_identity_for_test(cache_identity); let path = dirs::cache_dir() .expect("platform cache dir") .join("github-copilot-sdk") @@ -136,8 +138,8 @@ fn extracted_binary_present_at_conventional_path() { } #[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))] -fn sanitize_version_for_test(version: &str) -> String { - version +fn sanitize_cache_identity_for_test(cache_identity: &str) -> String { + cache_identity .chars() .map(|c| match c { 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => c, @@ -222,7 +224,11 @@ fn pin_file_when_present_is_well_formed() { continue; } let contents = std::fs::read_to_string(&pin).expect("read CLI version snapshot"); - let mut saw_version = false; + let version = contents + .lines() + .filter_map(|line| line.split_once('=')) + .find_map(|(key, value)| (key.trim() == "version").then_some(value.trim())) + .unwrap_or_else(|| panic!("{filename} missing `version=` line")); let mut package_count = 0; for raw in contents.lines() { let line = raw.trim(); @@ -234,7 +240,19 @@ fn pin_file_when_present_is_well_formed() { .unwrap_or_else(|| panic!("malformed line: {raw:?}")); assert!(!value.trim().is_empty(), "empty value for key {key:?}"); if key.trim() == "version" { - saw_version = true; + continue; + } else if key.trim() == "release-url" { + assert!( + value.trim() + == format!( + "https://github.com/github/copilot-cli/releases/download/v{version}" + ) + || value.trim() + == format!( + "https://github.com/github/copilot-sdk/releases/download/runtime-{version}" + ), + "unexpected release URL in {filename}" + ); } else { assert_eq!( value.trim().len(), @@ -248,7 +266,6 @@ fn pin_file_when_present_is_well_formed() { package_count += 1; } } - assert!(saw_version, "{filename} missing `version=` line"); assert_eq!( package_count, 8, "{filename} has incomplete platform hashes" diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs index 3ecd399f6f..8c4e912bb5 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -78,6 +78,10 @@ mod session_fs_sqlite; mod session_lifecycle; #[path = "e2e/session_todos_changed.rs"] mod session_todos_changed; +#[path = "e2e/set_tools.rs"] +mod set_tools; +#[path = "e2e/skill_provider.rs"] +mod skill_provider; #[path = "e2e/skills.rs"] mod skills; #[path = "e2e/streaming_fidelity.rs"] diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index 37b5383583..c516f84be6 100644 --- a/rust/tests/e2e/client_options.rs +++ b/rust/tests/e2e/client_options.rs @@ -9,10 +9,10 @@ use github_copilot_sdk::session_events::{ }; use github_copilot_sdk::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, CliProgram, Client, ClientOptions, CloudSessionOptions, + AttachmentSelectionRange, CliProgram, Client, ClientMode, ClientOptions, CloudSessionOptions, CloudSessionRepository, CopilotExpAssignmentResponse, DeliveryMode, ExtensionInfo, GitHubReferenceType, MessageOptions, MessageSource, ProviderConfig, ResumeSessionConfig, - SessionConfig, SessionId, Transport, + SessionConfig, SessionId, TranscriptRecovery, Transport, }; use serde::Deserialize; use serde_json::{Value, json}; @@ -215,6 +215,50 @@ async fn should_forward_singular_provider_configuration_on_session_creation() { assert_eq!(provider["headers"]["x-provider"], json!("rust")); } +#[tokio::test] +async fn resume_transcript_recovery_defaults_overrides_and_projection() { + for mode in [ClientMode::Empty, ClientMode::CopilotCli] { + for choice in [None, Some(false), Some(true)] { + let fake = FakeCli::new(); + let reports_recovery = choice.unwrap_or(true); + let behavior = if reports_recovery { + "transcript-recovery" + } else { + "normal" + }; + let client = Client::start( + fake.client_options_with_behavior("recovery-client-token", behavior) + .with_mode(mode) + .with_base_directory(fake.path("state")), + ) + .await + .expect("start fake CLI client"); + let mut config = ResumeSessionConfig::new("recovery-session".into()) + .with_available_tools(Vec::::new()); + if let Some(allow) = choice { + config = config.with_allow_transcript_recovery(allow); + } + let session = client.resume_session(config).await.expect("resume session"); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + + let request = fake.captured_request("session.resume"); + let expected = choice.map(Value::Bool); + assert_eq!( + request.params.get("allowTranscriptRecovery"), + expected.as_ref(), + "mode: {mode:?}, choice: {choice:?}" + ); + let recovery = reports_recovery.then(|| TranscriptRecovery { + planned_backup_path: "recovery-backup.jsonl".into(), + invalid_line_numbers: vec![3, 5], + session_start_moved: true, + }); + assert_eq!(session.transcript_recovery(), recovery.as_ref()); + } + } +} + #[tokio::test] async fn should_forward_advanced_session_resume_options_to_the_cli() { let fake = FakeCli::new(); @@ -1138,7 +1182,15 @@ function handleMessage(message) { } if (message.method === "session.resume") { const sessionId = (message.params && message.params.sessionId) || "fake-session"; - writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null, openCanvases: [] }); + const result = { sessionId, workspacePath: null, capabilities: null, openCanvases: [] }; + if (behavior === "transcript-recovery") { + result.transcriptRecovery = { + plannedBackupPath: "recovery-backup.jsonl", + invalidLineNumbers: [3, 5], + sessionStartMoved: true, + }; + } + writeResponse(message.id, result); return; } if (message.method === "session.options.update") { diff --git a/rust/tests/e2e/commands.rs b/rust/tests/e2e/commands.rs index 4fb4b69b78..68b23198a6 100644 --- a/rust/tests/e2e/commands.rs +++ b/rust/tests/e2e/commands.rs @@ -253,4 +253,4 @@ fn assert_command( assert!(!command.description.trim().is_empty()); } static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("commands", 4); + super::support::SharedE2eGroup::standard("commands", 3); diff --git a/rust/tests/e2e/session_fs.rs b/rust/tests/e2e/session_fs.rs index 8b13789179..479a07d935 100644 --- a/rust/tests/e2e/session_fs.rs +++ b/rust/tests/e2e/session_fs.rs @@ -1 +1,324 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use base64::Engine; +use github_copilot_sdk::session_fs::{FsError, SessionFsBinaryProvider}; +use github_copilot_sdk::{ + Client, DirEntry, DirEntryKind, FileInfo, SessionFsCapabilities, SessionFsConfig, + SessionFsConventions, SessionFsProvider, +}; + +struct BinaryFileProvider { + root: PathBuf, + read_paths: Mutex>, +} + +impl BinaryFileProvider { + fn path(&self, path: &str) -> PathBuf { + self.root.join(path.trim_start_matches('/')) + } +} + +#[async_trait] +impl SessionFsBinaryProvider for BinaryFileProvider { + async fn read_file_bytes(&self, path: &str) -> Result, FsError> { + self.read_paths.lock().unwrap().push(path.to_string()); + tokio::fs::read(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn write_file_bytes( + &self, + path: &str, + content: &[u8], + _mode: Option, + ) -> Result<(), FsError> { + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::write(target, content) + .await + .map_err(FsError::from) + } +} + +#[async_trait] +impl SessionFsProvider for BinaryFileProvider { + fn binary(&self) -> Option<&dyn SessionFsBinaryProvider> { + Some(self) + } + + async fn read_file(&self, path: &str) -> Result { + tokio::fs::read_to_string(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn write_file( + &self, + path: &str, + content: &str, + _mode: Option, + ) -> Result<(), FsError> { + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::write(target, content) + .await + .map_err(FsError::from) + } + + async fn append_file( + &self, + path: &str, + content: &str, + _mode: Option, + ) -> Result<(), FsError> { + use tokio::io::AsyncWriteExt; + + let target = self.path(path); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(target) + .await + .map_err(FsError::from)?; + file.write_all(content.as_bytes()) + .await + .map_err(FsError::from) + } + + async fn exists(&self, path: &str) -> Result { + tokio::fs::try_exists(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn stat(&self, path: &str) -> Result { + let metadata = tokio::fs::metadata(self.path(path)) + .await + .map_err(FsError::from)?; + Ok(FileInfo::new( + metadata.is_file(), + metadata.is_dir(), + metadata.len() as i64, + "1970-01-01T00:00:00Z", + "1970-01-01T00:00:00Z", + )) + } + + async fn mkdir(&self, path: &str, _recursive: bool, _mode: Option) -> Result<(), FsError> { + tokio::fs::create_dir_all(self.path(path)) + .await + .map_err(FsError::from) + } + + async fn readdir(&self, path: &str) -> Result, FsError> { + let mut dir = tokio::fs::read_dir(self.path(path)) + .await + .map_err(FsError::from)?; + let mut names = Vec::new(); + while let Some(entry) = dir.next_entry().await.map_err(FsError::from)? { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + Ok(names) + } + + async fn readdir_with_types(&self, path: &str) -> Result, FsError> { + let mut dir = tokio::fs::read_dir(self.path(path)) + .await + .map_err(FsError::from)?; + let mut entries = Vec::new(); + while let Some(entry) = dir.next_entry().await.map_err(FsError::from)? { + let kind = if entry.file_type().await.map_err(FsError::from)?.is_dir() { + DirEntryKind::Directory + } else { + DirEntryKind::File + }; + entries.push(DirEntry::new(entry.file_name().to_string_lossy(), kind)); + } + Ok(entries) + } + + async fn rm(&self, path: &str, recursive: bool, force: bool) -> Result<(), FsError> { + let target = self.path(path); + let result = if recursive { + tokio::fs::remove_dir_all(target).await + } else { + tokio::fs::remove_file(target).await + }; + match result { + Err(err) if force && err.kind() == std::io::ErrorKind::NotFound => Ok(()), + result => result.map_err(FsError::from), + } + } + + async fn rename(&self, src: &str, dest: &str) -> Result<(), FsError> { + let target = self.path(dest); + tokio::fs::create_dir_all(target.parent().unwrap()) + .await + .map_err(FsError::from)?; + tokio::fs::rename(self.path(src), target) + .await + .map_err(FsError::from) + } +} + +#[tokio::test] +async fn should_route_file_operations_through_the_session_fs_provider() { + super::support::with_e2e_context( + "session_fs", + "should_route_file_operations_through_the_session_fs_provider", + |ctx| { + Box::pin(async move { + let provider = Arc::new(BinaryFileProvider { + root: ctx.work_dir().join("text-provider"), + read_paths: Mutex::new(Vec::new()), + }); + let session_state_path = if cfg!(windows) { + "/session-state".to_string() + } else { + ctx.work_dir() + .join("session-state") + .to_string_lossy() + .into_owned() + }; + let client = Client::start( + ctx.client_options().with_session_fs( + SessionFsConfig::new( + "/", + session_state_path.clone(), + SessionFsConventions::Posix, + ) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)), + ), + ) + .await + .expect("start session client"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_session_fs_provider(provider.clone()), + ) + .await + .expect("create session"); + + let answer = session + .send_and_wait("What is 100 + 200?") + .await + .expect("send arithmetic request") + .expect("final assistant message"); + assert!( + answer.data["content"] + .as_str() + .is_some_and(|content| content.contains("300")), + "unexpected assistant response: {:?}", + answer.data + ); + session.disconnect().await.expect("disconnect session"); + let events_path = provider.path(&format!("{session_state_path}/events.jsonl")); + let events = tokio::fs::read_to_string(&events_path) + .await + .unwrap_or_else(|err| { + panic!("read provider events {}: {err}", events_path.display()) + }); + assert!( + events.contains("300"), + "provider events did not contain the response" + ); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_view_an_image_that_exists_only_in_the_binary_session_fs_provider() { + super::support::with_e2e_context( + "session_fs", + "should_view_an_image_that_exists_only_in_the_binary_session_fs_provider", + |ctx| { + Box::pin(async move { + let image_path = "/sdk-provider-image.png"; + let image_bytes = base64::engine::general_purpose::STANDARD + .decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==") + .unwrap(); + let provider = Arc::new(BinaryFileProvider { + root: ctx.work_dir().join("binary-provider"), + read_paths: Mutex::new(Vec::new()), + }); + let session_state_path = if cfg!(windows) { + "/session-state".to_string() + } else { + ctx.work_dir() + .join("session-state") + .to_string_lossy() + .into_owned() + }; + let client = Client::start( + ctx.client_options().with_session_fs( + SessionFsConfig::new("/", session_state_path, SessionFsConventions::Posix) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)), + ), + ) + .await + .expect("start binary session client"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_session_fs_provider(provider.clone()), + ) + .await + .expect("create binary session"); + let stored_path = provider.path(image_path); + tokio::fs::create_dir_all(stored_path.parent().unwrap()) + .await + .unwrap(); + tokio::fs::write(stored_path, &image_bytes).await.unwrap(); + assert!(!Path::new(image_path).exists()); + + let answer = session + .send_and_wait( + "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + ) + .await + .expect("send image view request") + .expect("final assistant message"); + assert!( + answer.data["content"] + .as_str() + .is_some_and(|content| content.contains("SDK_PROVIDER_IMAGE_DONE")), + "unexpected assistant response: {:?}", + answer.data + ); + assert!( + provider.read_paths.lock().unwrap().iter().any(|p| p == image_path), + "view did not request provider image bytes" + ); + let events = session.get_events().await.expect("get session events"); + assert!( + events.iter().any(|event| { + event.event_type == "session.binary_asset" + && event.data["mimeType"] == "image/png" + && event.data["data"] + == base64::engine::general_purpose::STANDARD.encode(&image_bytes) + }), + "missing image asset with exact provider bytes" + ); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} diff --git a/rust/tests/e2e/set_tools.rs b/rust/tests/e2e/set_tools.rs new file mode 100644 index 0000000000..c8ebb29bc9 --- /dev/null +++ b/rust/tests/e2e/set_tools.rs @@ -0,0 +1,316 @@ +use std::sync::Arc; + +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::tool::ToolHandler; +use github_copilot_sdk::{Error, ResumeSessionConfig, Tool, ToolInvocation, ToolResult}; +use serde_json::json; + +use super::support::assistant_message_content; + +// Shared with the other SDKs' set_tools E2E tests, which replay the same +// snapshots. +const FRUIT_PROMPT: &str = "Use lookup_fruit to find the fruit for code 42."; +const FRUIT_AND_VEGETABLE_PROMPT: &str = "Use lookup_fruit to find the fruit for code 42 again, and use lookup_vegetable to find the vegetable for code 7."; +const VEGETABLE_PROMPT: &str = "Use lookup_vegetable to find the vegetable for code 7."; + +/// Answers with a fixed result and records the codes it was called with. +struct LookupTool { + result: &'static str, + calls: Arc>>, +} + +#[async_trait::async_trait] +impl ToolHandler for LookupTool { + async fn call(&self, invocation: ToolInvocation) -> Result { + if let Some(code) = invocation + .arguments + .get("code") + .and_then(|code| code.as_i64()) + { + self.calls.lock().push(code); + } + Ok(ToolResult::Text(self.result.to_string())) + } +} + +type Calls = Arc>>; + +fn lookup_tool( + name: &str, + description: &str, + code_description: &str, + result: &'static str, +) -> (Tool, Calls) { + let calls = Calls::default(); + let tool = Tool::new(name) + .with_description(description) + .with_parameters(json!({ + "type": "object", + "properties": { + "code": { "type": "integer", "description": code_description } + }, + "required": ["code"] + })) + .with_handler(Arc::new(LookupTool { + result, + calls: calls.clone(), + })); + (tool, calls) +} + +fn lookup_fruit(fruit: &'static str) -> (Tool, Calls) { + lookup_tool( + "lookup_fruit", + "Looks up the fruit for a numeric code", + "Fruit code", + fruit, + ) +} + +fn lookup_vegetable() -> (Tool, Calls) { + lookup_tool( + "lookup_vegetable", + "Looks up the vegetable for a numeric code", + "Vegetable code", + "carrot", + ) +} + +fn calls(calls: &Calls) -> Vec { + calls.lock().clone() +} + +/// A tool with no parameters that answers with a fixed result. +fn plain_tool(name: &str, description: &str, result: &'static str) -> Tool { + Tool::new(name) + .with_description(description) + .with_handler(Arc::new(LookupTool { + result, + calls: Calls::default(), + })) +} + +/// Index of the first model request that carries `prompt` as a user message. +fn index_of_prompt(exchanges: &[serde_json::Value], prompt: &str) -> Option { + exchanges.iter().position(|exchange| { + exchange["request"]["messages"] + .as_array() + .into_iter() + .flatten() + .any(|message| { + let content = &message["content"]; + message["role"] == "user" + && content.as_str().map_or_else( + || content.to_string().contains(prompt), + |text| text.contains(prompt), + ) + }) + }) +} + +/// Asserts that every model request offered the tools in `offered` and none of +/// the tools in `not_offered`. +fn assert_offered(exchanges: &[serde_json::Value], offered: &[&str], not_offered: &[&str]) { + for exchange in exchanges { + let tools: Vec<&str> = exchange["request"]["tools"] + .as_array() + .into_iter() + .flatten() + .filter_map(|tool| tool["function"]["name"].as_str()) + .collect(); + for name in offered { + assert!(tools.contains(name), "offered {tools:?}, expected {name}"); + } + for name in not_offered { + assert!( + !tools.contains(name), + "offered {tools:?}, expected no {name}" + ); + } + } +} + +#[tokio::test] +async fn replaces_tools_on_a_created_session() { + super::support::with_shared_e2e_context( + &E2E, + "set_tools", + "replaces_tools_on_a_created_session", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (original, original_calls) = lookup_fruit("apple"); + let retired = plain_tool("retired_lookup", "Looks up a retired value", "retired"); + let session = client + .create_session( + ctx.approve_all_session_config() + .with_tools(vec![original, retired]), + ) + .await + .expect("create session"); + + let first = session + .send_and_wait(FRUIT_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&first).contains("apple")); + + let (replacement, replacement_calls) = lookup_fruit("dragonfruit"); + let (vegetable, vegetable_calls) = lookup_vegetable(); + session + .set_tools([replacement, vegetable]) + .await + .expect("replace tools"); + + let second = session + .send_and_wait(FRUIT_AND_VEGETABLE_PROMPT) + .await + .expect("send") + .expect("assistant message"); + let content = assistant_message_content(&second); + assert!(content.contains("dragonfruit"), "{content}"); + assert!(content.contains("carrot"), "{content}"); + assert_eq!(calls(&original_calls), [42]); + assert_eq!(calls(&replacement_calls), [42]); + assert_eq!(calls(&vegetable_calls), [7]); + + // Model requests after the replacement offer exactly the new tool set. + let exchanges = ctx.exchanges(); + let replaced_from = index_of_prompt(&exchanges, FRUIT_AND_VEGETABLE_PROMPT) + .expect("a model request carries the replacement prompt"); + assert!( + replaced_from > 0, + "expected model requests before the replacement" + ); + let (before, after) = exchanges.split_at(replaced_from); + assert_offered( + before, + &["lookup_fruit", "retired_lookup"], + &["lookup_vegetable"], + ); + assert_offered( + after, + &["lookup_fruit", "lookup_vegetable"], + &["retired_lookup"], + ); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn replaces_tools_on_a_resumed_session() { + super::support::with_shared_e2e_context( + &E2E, + "set_tools", + "replaces_tools_on_a_resumed_session", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (created_tool, created_calls) = lookup_fruit("apple"); + let created = client + .create_session( + ctx.approve_all_session_config() + .with_tools(vec![created_tool]), + ) + .await + .expect("create session"); + let session_id = created.id().clone(); + let first = created + .send_and_wait(FRUIT_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&first).contains("apple")); + assert_eq!(calls(&created_calls), [42]); + created.disconnect().await.expect("disconnect session"); + + let (fruit, fruit_calls) = lookup_fruit("apple"); + let resumed = client + .resume_session( + ResumeSessionConfig::new(session_id) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_tools(vec![fruit]), + ) + .await + .expect("resume session"); + let (vegetable, vegetable_calls) = lookup_vegetable(); + resumed.set_tools([vegetable]).await.expect("replace tools"); + + let answer = resumed + .send_and_wait(VEGETABLE_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&answer).contains("carrot")); + assert_eq!(calls(&vegetable_calls), [7]); + assert!(calls(&fruit_calls).is_empty()); + + let exchanges = ctx.exchanges(); + let replaced_from = index_of_prompt(&exchanges, VEGETABLE_PROMPT) + .expect("a model request carries the replacement prompt"); + assert!( + replaced_from > 0, + "expected model requests before the replacement" + ); + let (_, after) = exchanges.split_at(replaced_from); + assert_offered(after, &["lookup_vegetable"], &["lookup_fruit"]); + + resumed.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn keeps_the_previous_tools_when_a_replacement_is_rejected() { + super::support::with_shared_e2e_context( + &E2E, + "set_tools", + "keeps_the_previous_tools_when_a_replacement_is_rejected", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (original, original_calls) = lookup_fruit("apple"); + let session = client + .create_session(ctx.approve_all_session_config().with_tools(vec![original])) + .await + .expect("create session"); + + let (replacement, replacement_calls) = lookup_fruit("dragonfruit"); + let invalid = plain_tool("invalid.tool", "Has a name the runtime rejects", "never"); + session + .set_tools([replacement, invalid]) + .await + .expect_err("the runtime rejects an invalid tool name"); + + let answer = session + .send_and_wait(FRUIT_PROMPT) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&answer).contains("apple")); + assert_eq!(calls(&original_calls), [42]); + assert!(calls(&replacement_calls).is_empty()); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +static E2E: super::support::SharedE2eGroup = + super::support::SharedE2eGroup::standard("set_tools", 3); diff --git a/rust/tests/e2e/skill_provider.rs b/rust/tests/e2e/skill_provider.rs new file mode 100644 index 0000000000..2bbbac7814 --- /dev/null +++ b/rust/tests/e2e/skill_provider.rs @@ -0,0 +1,641 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::rpc::SkillSource; +use github_copilot_sdk::session_events::{SessionEventType, ToolExecutionCompleteData}; +use github_copilot_sdk::{ + CloudSessionOptions, Error, ErrorKind, ResumeSessionConfig, SessionConfig, SessionEvent, + SkillProvider, SkillProviderDescriptor, +}; +use tokio::sync::Notify; + +use super::support::{assistant_message_content, collect_until_idle}; + +static E2E: super::support::SharedE2eGroup = + super::support::SharedE2eGroup::standard("skill_provider", 5); + +#[tokio::test] +async fn should_load_provider_skill_lazily_through_skill_tool() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_load_provider_skill_lazily_through_skill_tool", + |ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "provider-lookup", + "Reports the provider lookup verification word.", + "# Provider lookup\n\nThe verification word is TANGERINE_QUARTZ_19. Reply with it.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + let skills = session.rpc().skills().list().await.expect("list skills"); + let listed = skills + .skills + .iter() + .find(|skill| skill.name == "provider-lookup") + .expect("provider skill"); + assert_eq!(listed.source, SkillSource::Sdk); + assert!(listed.enabled); + assert_eq!(listed.path.as_deref().unwrap_or_default(), ""); + assert_eq!(provider.reads(), Vec::::new()); + + let message = session + .send_and_wait( + "Use the skill tool to load the provider-lookup skill, then reply with its verification word.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(provider.reads(), vec!["provider-lookup"]); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("TANGERINE_QUARTZ_19")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_load_provider_and_file_based_skills_together() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_load_provider_and_file_based_skills_together", + |ctx| { + Box::pin(async move { + let skills_dir = write_file_notes_skill(ctx.work_dir()); + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "provider-audit", + "Reports the provider audit verification word.", + "---\nname: provider-audit\nallowed-tools: view\n---\n\nThe provider audit verification word is COBALT_HERON_58.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_directories([skills_dir]) + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + let skills = session.rpc().skills().list().await.expect("list skills"); + let file_skill = skills + .skills + .iter() + .find(|skill| skill.name == "file-notes") + .expect("file skill"); + let provider_skill = skills + .skills + .iter() + .find(|skill| skill.name == "provider-audit") + .expect("provider skill"); + assert_ne!(file_skill.source, SkillSource::Sdk); + assert!(file_skill.path.as_deref().is_some_and(|path| !path.is_empty())); + assert_eq!(provider_skill.source, SkillSource::Sdk); + + let message = session + .send_and_wait( + "Use the skill tool to load the file-notes skill and the provider-audit skill, then reply with both verification words.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(provider.reads(), vec!["provider-audit"]); + // Validate the final assistant response arrived (guards against truncated captures) + let content = assistant_message_content(&message); + assert!(content.contains("MAPLE_FALCON_27")); + assert!(content.contains("COBALT_HERON_58")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_rebind_skill_provider_on_resume() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_rebind_skill_provider_on_resume", + |ctx| { + Box::pin(async move { + let original = Arc::new(TestSkillProvider::new(vec![skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is AMBER_ALPHA_11.\n", + )])); + let replacement = Arc::new(TestSkillProvider::new(vec![skill( + "rebind-check", + "Reports the rebind verification word.", + "The rebind verification word is BRONZE_BETA_22.\n", + )])); + let client = ctx.start_client().await; + let first = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(original.clone()), + ) + .await + .expect("create session"); + let session_id = first.id().clone(); + let ready = first + .send_and_wait( + "Without using any tools or skills, reply with exactly REBIND_READY.", + ) + .await + .expect("send readiness turn") + .expect("assistant message"); + assert!(assistant_message_content(&ready).contains("REBIND_READY")); + first.disconnect().await.expect("disconnect first session"); + assert_eq!(original.reads(), Vec::::new()); + let original_calls_before_resume = original.calls().len(); + + let session = client + .resume_session( + ResumeSessionConfig::new(session_id) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_skill_provider(replacement.clone()), + ) + .await + .expect("resume session"); + + let message = session + .send_and_wait( + "Use the skill tool to load the rebind-check skill, then reply with its verification word.", + ) + .await + .expect("send") + .expect("assistant message"); + + assert_eq!(replacement.reads(), vec!["rebind-check"]); + assert_eq!(original.calls().len(), original_calls_before_resume); + // Validate the final assistant response arrived (guards against truncated captures) + let content = assistant_message_content(&message); + assert!(content.contains("BRONZE_BETA_22")); + assert!(!content.contains("AMBER_ALPHA_11")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_report_provider_read_failure_without_leaking_details() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_report_provider_read_failure_without_leaking_details", + |ctx| { + Box::pin(async move { + let secret = "PROVIDER_SECRET_7F3A9C"; + let provider = Arc::new(TestSkillProvider::new(vec![ProvidedSkill::new( + "broken-lookup", + "Reports the broken lookup verification word.", + SkillRead::Error(format!("database unavailable: {secret}")), + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let events = session.subscribe(); + + let message = session + .send_and_wait( + "Use the skill tool to load the broken-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + ) + .await + .expect("send") + .expect("assistant message"); + let observed = collect_until_idle(events).await; + + assert!(provider.reads().contains(&"broken-lookup".to_string())); + let failures = failed_tool_executions(&observed); + assert_eq!(failures.len(), 1, "expected one failed tool execution"); + assert_no_secret(&observed, secret); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("LOAD_FAILED")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_report_missing_provider_skill_as_not_found() { + super::support::with_shared_e2e_context( + &E2E, + "skill_provider", + "should_report_missing_provider_skill_as_not_found", + |ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![ProvidedSkill::new( + "vanished-lookup", + "Reports the vanished lookup verification word.", + SkillRead::Missing, + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let events = session.subscribe(); + + let message = session + .send_and_wait( + "Use the skill tool to load the vanished-lookup skill. If loading fails, reply with exactly LOAD_FAILED.", + ) + .await + .expect("send") + .expect("assistant message"); + let observed = collect_until_idle(events).await; + + assert!(provider.reads().contains(&"vanished-lookup".to_string())); + let failures = failed_tool_executions(&observed); + assert_eq!(failures.len(), 1, "expected one failed tool execution"); + let failure = serde_json::to_string(&failures[0]).expect("serialize failure"); + assert!( + failure.to_ascii_lowercase().contains("not found"), + "expected not found failure, got {failure}" + ); + // Validate the final assistant response arrived (guards against truncated captures) + assert!(assistant_message_content(&message).contains("LOAD_FAILED")); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_keep_provider_dormant_when_skills_disabled() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "dormant-lookup", + "Never listed.", + "Never read.\n", + )])); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_enable_skills(false) + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + + session + .rpc() + .skills() + .ensure_loaded() + .await + .expect("ensure skills loaded"); + let skills = session.rpc().skills().list().await.expect("list skills"); + + assert_no_sdk_skills(&skills.skills); + assert_eq!(provider.calls(), Vec::::new()); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +#[tokio::test] +async fn should_unbind_provider_when_resumed_without_one() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "unbound-lookup", + "Reports the unbound lookup word.", + "Unbound.\n", + )])); + let client = ctx.start_client().await; + let first = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(provider.clone()), + ) + .await + .expect("create session"); + let before = first.rpc().skills().list().await.expect("list skills"); + assert!( + before + .skills + .iter() + .any(|skill| skill.name == "unbound-lookup") + ); + let calls_before_resume = provider.calls().len(); + + let session = client + .resume_session( + ResumeSessionConfig::new(first.id().clone()) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN), + ) + .await + .expect("resume session"); + + session + .rpc() + .skills() + .reload() + .await + .expect("reload skills"); + let skills = session.rpc().skills().list().await.expect("list skills"); + + assert_no_sdk_skills(&skills.skills); + assert_eq!(provider.calls().len(), calls_before_resume); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + drop(first); + }) + }) + .await; +} + +#[tokio::test] +async fn should_cancel_a_blocked_provider_call_when_the_session_disconnects() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_skill_provider(Arc::new(BlockingSkillProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await + .expect("create session"); + + // The list RPC fails once the binding is removed; only the provider's + // cancellation matters here. + let rpc = session.rpc(); + let skills = rpc.skills(); + let list = skills.list(); + tokio::pin!(list); + tokio::select! { + _ = entered.notified() => {} + _ = &mut list => panic!("skills.list finished before the provider was called"), + _ = tokio::time::sleep(Duration::from_secs(30)) => { + panic!("provider list_skills was not called"); + } + } + + session.disconnect().await.expect("disconnect session"); + tokio::time::timeout(Duration::from_secs(10), dropped.notified()) + .await + .expect("provider future was not dropped after disconnect"); + + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +#[tokio::test] +async fn should_reject_skill_provider_for_cloud_sessions() { + super::support::with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + let provider = Arc::new(TestSkillProvider::new(vec![skill( + "cloud-lookup", + "Never listed.", + "Never read.\n", + )])); + let client = ctx.start_client().await; + + let result = client + .create_session( + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN) + .with_cloud(CloudSessionOptions::default()) + .with_skill_provider(provider.clone()), + ) + .await; + + match result { + Ok(session) => { + let _ = session.disconnect().await; + panic!("cloud session with skill provider unexpectedly succeeded"); + } + Err(error) => { + assert!( + error + .to_string() + .contains("Skill providers are not supported for cloud sessions."), + "unexpected error: {error}" + ); + } + } + assert_eq!(provider.calls(), Vec::::new()); + + client.stop().await.expect("stop client"); + }) + }) + .await; +} + +struct ProvidedSkill { + descriptor: SkillProviderDescriptor, + read: SkillRead, +} + +impl ProvidedSkill { + fn new(name: &str, description: &str, read: SkillRead) -> Self { + Self { + descriptor: descriptor(name, description), + read, + } + } +} + +enum SkillRead { + Markdown(&'static str), + Missing, + Error(String), +} + +struct TestSkillProvider { + skills: Vec, + calls: Mutex>, +} + +impl TestSkillProvider { + fn new(skills: Vec) -> Self { + Self { + skills, + calls: Mutex::new(Vec::new()), + } + } + + fn calls(&self) -> Vec { + self.calls.lock().expect("provider call log").clone() + } + + fn reads(&self) -> Vec { + self.calls() + .into_iter() + .filter_map(|call| call.strip_prefix("read:").map(str::to_string)) + .collect() + } + + fn push_call(&self, call: impl Into) { + self.calls + .lock() + .expect("provider call log") + .push(call.into()); + } +} + +#[async_trait] +impl SkillProvider for TestSkillProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + self.push_call("list"); + Ok(self + .skills + .iter() + .map(|skill| skill.descriptor.clone()) + .collect()) + } + + async fn read_skill(&self, name: &str) -> std::result::Result, Error> { + self.push_call(format!("read:{name}")); + let Some(skill) = self + .skills + .iter() + .find(|skill| skill.descriptor.name == name) + else { + return Ok(None); + }; + + match &skill.read { + SkillRead::Markdown(markdown) => Ok(Some((*markdown).to_string())), + SkillRead::Missing => Ok(None), + SkillRead::Error(message) => Err(Error::with_message( + ErrorKind::InvalidConfig, + message.clone(), + )), + } + } +} + +fn skill(name: &str, description: &str, markdown: &'static str) -> ProvidedSkill { + ProvidedSkill::new(name, description, SkillRead::Markdown(markdown)) +} + +struct NotifyOnDrop(Arc); + +impl Drop for NotifyOnDrop { + fn drop(&mut self) { + self.0.notify_one(); + } +} + +/// Blocks `list_skills` until the SDK drops its future. +struct BlockingSkillProvider { + entered: Arc, + dropped: Arc, +} + +#[async_trait] +impl SkillProvider for BlockingSkillProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + Ok(None) + } +} + +fn descriptor(name: &str, description: &str) -> SkillProviderDescriptor { + SkillProviderDescriptor { + name: name.to_string(), + description: description.to_string(), + ..Default::default() + } +} + +fn write_file_notes_skill(work_dir: &Path) -> std::path::PathBuf { + let skills_dir = work_dir.join("file-skills"); + let skill_dir = skills_dir.join("file-notes"); + std::fs::create_dir_all(&skill_dir).expect("create skill directory"); + std::fs::write( + skill_dir.join("SKILL.md"), + "---\nname: file-notes\ndescription: Reports the file notes verification word.\n---\n\nThe file notes verification word is MAPLE_FALCON_27.\n", + ) + .expect("write file skill"); + skills_dir +} + +fn failed_tool_executions(events: &[SessionEvent]) -> Vec { + events + .iter() + .filter(|event| event.parsed_type() == SessionEventType::ToolExecutionComplete) + .filter_map(|event| event.typed_data::()) + .filter(|data| !data.success) + .collect() +} + +fn assert_no_secret(events: &[SessionEvent], secret: &str) { + let events_json = serde_json::to_string(events).expect("serialize events"); + assert!( + !events_json.contains(secret), + "provider secret leaked into events: {events_json}" + ); +} + +fn assert_no_sdk_skills(skills: &[github_copilot_sdk::rpc::Skill]) { + assert!( + skills.iter().all(|skill| skill.source != SkillSource::Sdk), + "expected no SDK skills, got {skills:?}" + ); +} diff --git a/rust/tests/e2e/subagent_hooks.rs b/rust/tests/e2e/subagent_hooks.rs index f106223e5a..56cad894cb 100644 --- a/rust/tests/e2e/subagent_hooks.rs +++ b/rust/tests/e2e/subagent_hooks.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use async_trait::async_trait; use github_copilot_sdk::hooks::{ HookContext, PostToolUseInput, PostToolUseOutput, PreToolUseInput, PreToolUseOutput, - SessionHooks, + SessionHooks, SubagentStartInput, SubagentStartOutput, SubagentStopInput, SubagentStopOutput, }; use github_copilot_sdk::session_events::SessionEventType; use github_copilot_sdk::{ @@ -16,14 +16,17 @@ use tokio::sync::watch; use super::support::{assistant_message_content, wait_for_event, with_e2e_context}; +const CHILD_CONTEXT: &str = "Subagent start hook verified: read the requested file."; +const STOP_RESPONSE_PREFIX: &str = "Subagent stop hook verified: "; + #[tokio::test] -async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls() { +async fn should_apply_subagent_lifecycle_hook_outputs() { if super::support::skip_inprocess("LLM inference providers are process-global in-process") { return; } with_e2e_context( "subagent_hooks", - "should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls", + "should_apply_subagent_lifecycle_hook_outputs", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); @@ -34,6 +37,8 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls .expect("write test file"); let hook_log = Arc::new(Mutex::new(Vec::::new())); + let subagent_starts = Arc::new(Mutex::new(Vec::new())); + let subagent_stops = Arc::new(Mutex::new(Vec::new())); let request_log = Arc::new(RecordingRequestHandler::default()); let waiting_text = "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete..."; let final_text = "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; @@ -51,6 +56,8 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls RecordingHooks { log: Arc::clone(&hook_log), parent_reply_observed, + subagent_starts: Arc::clone(&subagent_starts), + subagent_stops: Arc::clone(&subagent_stops), }, ))) .await @@ -133,7 +140,76 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls task_pre.unwrap().session_id, "Sub-agent tool hooks should have a different sessionId than parent tool hooks" ); - assert_subagent_request_metadata(&request_log.inference_records()); + let requests = request_log.inference_records(); + assert_subagent_request_metadata(&requests); + let context_and_task = format!("{CHILD_CONTEXT}\n\nRead the file \"subagent-test.txt\""); + let prompt_has_context = |prompt: &str| prompt.contains(context_and_task.as_str()); + assert!( + requests + .iter() + .filter(|request| request.parent_agent_id.is_some()) + .any(|request| { + let body: serde_json::Value = serde_json::from_str(&request.body) + .expect("child inference request body"); + body["messages"].as_array().is_some_and(|messages| { + messages.iter().any(|message| { + message["role"] == "user" + && (message["content"] + .as_str() + .is_some_and(prompt_has_context) + || message["content"].as_array().is_some_and(|parts| { + parts.iter().any(|part| { + part["type"] == "text" + && part["text"] + .as_str() + .is_some_and(prompt_has_context) + }) + })) + }) + }) + }), + "start hook context should be prepended to the child inference prompt" + ); + assert!( + requests.iter().any(|request| { + request.parent_agent_id.is_none() + && request.body.contains(STOP_RESPONSE_PREFIX) + }), + "rewritten stop response should reach a parent inference request" + ); + + { + let starts = subagent_starts.lock(); + assert_eq!(starts.len(), 1, "one subagentStart per launched child"); + let start = &starts[0]; + assert_eq!(start.session_id, session.id().as_str()); + assert!(start.timestamp > 0.0); + assert_eq!( + start + .working_directory + .canonicalize() + .expect("canonical hook working directory"), + ctx.work_dir().canonicalize().expect("canonical test directory") + ); + assert_eq!(start.agent_name, "explore"); + assert_eq!(start.agent_display_name, None); + assert_eq!(start.agent_description, None); + + let stops = subagent_stops.lock(); + assert_eq!(stops.len(), 1, "one subagentStop per completed child"); + let stop = &stops[0]; + assert_eq!(stop.session_id, start.session_id); + assert!(stop.timestamp >= start.timestamp); + assert_eq!(stop.working_directory, start.working_directory); + assert_eq!(stop.transcript_path, start.transcript_path); + assert_eq!(stop.agent_name, start.agent_name); + assert_eq!(stop.agent_type, "explore"); + assert!(stop.agent_id.as_ref().is_some_and(|id| !id.is_empty())); + assert_eq!(stop.agent_display_name, start.agent_display_name); + assert_eq!(stop.agent_description, start.agent_description); + assert_eq!(stop.stop_reason, "end_turn"); + assert!(stop.response.contains("Hello from subagent test!")); + } session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); @@ -156,6 +232,7 @@ struct RequestEntry { agent_id: Option, parent_agent_id: Option, interaction_type: Option, + body: String, } #[derive(Default)] @@ -186,6 +263,13 @@ impl CopilotRequestHandler for RecordingRequestHandler { agent_id: ctx.agent_id.clone(), parent_agent_id: ctx.parent_agent_id.clone(), interaction_type: ctx.interaction_type.clone(), + body: if is_inference_url(&request.url) { + std::str::from_utf8(&request.body) + .expect("inference request body is UTF-8") + .to_owned() + } else { + String::new() + }, }); forward_http(request).await } @@ -237,6 +321,8 @@ fn assert_subagent_request_metadata(records: &[RequestEntry]) { struct RecordingHooks { log: Arc>>, parent_reply_observed: watch::Receiver, + subagent_starts: Arc>>, + subagent_stops: Arc>>, } fn is_subagent_view(tool_name: &str, session_id: &str, log: &[HookEntry]) -> bool { @@ -262,6 +348,30 @@ fn only_child_view_waits_for_parent_reply() { #[async_trait] impl SessionHooks for RecordingHooks { + async fn on_subagent_start( + &self, + input: SubagentStartInput, + _ctx: HookContext, + ) -> Option { + self.subagent_starts.lock().push(input); + Some(SubagentStartOutput { + additional_context: Some(CHILD_CONTEXT.to_string()), + }) + } + + async fn on_subagent_stop( + &self, + input: SubagentStopInput, + _ctx: HookContext, + ) -> Option { + let response = format!("{STOP_RESPONSE_PREFIX}{}", input.response); + self.subagent_stops.lock().push(input); + Some(SubagentStopOutput { + modified_response: Some(response), + ..SubagentStopOutput::default() + }) + } + async fn on_pre_tool_use( &self, input: PreToolUseInput, diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index d352aa3131..fa945bf584 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -1227,8 +1227,7 @@ fn cli_path(repo_root: &Path) -> std::io::Result { } } - let npm = if cfg!(windows) { "npm.cmd" } else { "npm" }; - let output = std::process::Command::new(npm) + let output = std::process::Command::new(npm_program()) .args(["run", "--silent", "prepare:runtime", "--", "--print-path"]) .current_dir(repo_root.join("nodejs")) .output()?; @@ -1571,6 +1570,28 @@ fn node_program() -> &'static str { if cfg!(windows) { "node.exe" } else { "node" } } +fn npm_program() -> &'static str { + if cfg!(windows) { + windows_program(&["npm.cmd", "npm.exe", "npm"]) + } else { + "npm" + } +} + +fn windows_program(candidates: &[&'static str]) -> &'static str { + candidates + .iter() + .copied() + .find(|candidate| program_exists_on_path(candidate)) + .unwrap_or(candidates[0]) +} + +fn program_exists_on_path(program: &str) -> bool { + std::env::var_os("PATH").is_some_and(|path| { + std::env::split_paths(&path).any(|directory| directory.join(program).is_file()) + }) +} + #[test] fn e2e_context_isolates_copilot_cache() { let home_dir = tempfile::tempdir().expect("create test home"); diff --git a/rust/tests/e2e/tools.rs b/rust/tests/e2e/tools.rs index 586a31d3a1..3a03a7df94 100644 --- a/rust/tests/e2e/tools.rs +++ b/rust/tests/e2e/tools.rs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + use std::sync::Arc; use github_copilot_sdk::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; @@ -11,6 +13,54 @@ use tokio::sync::{Mutex, mpsc}; use super::support::{assistant_message_content, recv_with_timeout}; +#[cfg(feature = "derive")] +#[tokio::test] +async fn string_schema_apply_patch_override_binds_patch_input() { + super::support::with_shared_e2e_context( + &E2E, + "tools", + "string_schema_apply_patch_override_binds_patch_input", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (tx, mut rx) = mpsc::unbounded_channel(); + let tool = github_copilot_sdk::tool::define_tool::( + "apply_patch", + "Apply a patch", + move |_invocation, input| { + let tx = tx.clone(); + async move { + tx.send(input).expect("capture patch"); + Ok(ToolResult::Text("HOST_PATCH_HANDLED".to_owned())) + } + }, + ) + .with_overrides_built_in_tool(true); + let session = client + .create_session(ctx.approve_all_session_config().with_tools(vec![tool])) + .await + .expect("create session"); + let response = session + .send_and_wait("Use apply_patch to apply the supplied patch.") + .await + .expect("send") + .expect("assistant message"); + + assert_eq!( + recv_with_timeout(&mut rx, "string-schema patch").await, + "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch" + ); + assert_eq!(assistant_message_content(&response), "Host override completed."); + assert!(!ctx.work_dir().join("override-marker.txt").exists()); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + #[tokio::test] async fn invokes_built_in_tools() { super::support::with_shared_e2e_context(&E2E, "tools", "invokes_built_in_tools", |ctx| { @@ -880,4 +930,7 @@ impl ToolHandler for DbQueryTool { )) } } -static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard("tools", 11); +static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard( + "tools", + if cfg!(feature = "derive") { 12 } else { 11 }, +); diff --git a/rust/tests/fixtures/connector-runtime/budget.ts b/rust/tests/fixtures/connector-runtime/budget.ts new file mode 100644 index 0000000000..21650d1b0a --- /dev/null +++ b/rust/tests/fixtures/connector-runtime/budget.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** Wall-clock budget the harness allows a single fixture child process. */ +export const CASE_TIMEOUT_MS = 210_000; + +/** Slack for process spawn, MCP/API server startup, and report writing. */ +const MATRIX_OVERHEAD_MS = 30_000; + +export const transports = (process.env.CONNECTOR_LOCAL_TRANSPORTS ?? "stdio,inprocess").split(","); +export const cases = (process.env.CONNECTOR_LOCAL_CASES ?? "static,rotation,scope,targeted,disabled").split(","); + +/** + * The matrix runs sequentially, so the suite timeout must cover every case's own + * budget rather than a single case's. + */ +export const matrixTimeoutMs = transports.length * cases.length * CASE_TIMEOUT_MS + MATRIX_OVERHEAD_MS; diff --git a/rust/tests/fixtures/connector-runtime/fixture.test.ts b/rust/tests/fixtures/connector-runtime/fixture.test.ts new file mode 100644 index 0000000000..0317b88770 --- /dev/null +++ b/rust/tests/fixtures/connector-runtime/fixture.test.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { it } from "../../../../nodejs/node_modules/vitest/dist/index.js"; + +import { matrixTimeoutMs } from "./budget.ts"; + +it( + "exercises the real public Rust SDK Connector contract", + async () => { + await import("./fixture.ts"); + }, + matrixTimeoutMs, +); diff --git a/rust/tests/fixtures/connector-runtime/fixture.ts b/rust/tests/fixtures/connector-runtime/fixture.ts new file mode 100644 index 0000000000..5c6f0a5b18 --- /dev/null +++ b/rust/tests/fixtures/connector-runtime/fixture.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { appendFile, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { CASE_TIMEOUT_MS, cases, transports } from "./budget.ts"; + +const runtimeRoot = process.env.CONNECTOR_FIXTURE_RUNTIME_ROOT; +const binary = process.env.CONNECTOR_LOCAL_BINARY; +assert(runtimeRoot && binary, "Supply the runtime fixture root and compiled SDK test binary"); +const { TestMcpHttpServer } = await import( + pathToFileURL(join(runtimeRoot, "test/cli/e2e/harness/testMcpHttpServer.ts")).href +); +const { getSharedCA } = await import(pathToFileURL(join(runtimeRoot, "test/cli/e2e/harness/certUtils.ts")).href); + +const tokens = { + first: "ghu_connector_session_first", + rotated: "ghu_connector_session_rotated", + other: "ghu_connector_session_other_user", + repository: "ghu_connector_repository_identity", +}; + +async function runCase(transport: string, testCase: string) { + const caPem = getSharedCA().certPem; + const directory = await mkdtemp(join(tmpdir(), "sdk-connector-local-")); + const home = join(directory, "home"); + await mkdir(home); + const ca = join(directory, "ca.pem"); + await writeFile(ca, caPem); + const mail = new TestMcpHttpServer(true, tokens.first); + const calendar = new TestMcpHttpServer(true, tokens.first); + const ordinary = new TestMcpHttpServer(true); + let mailUrl = ""; + let calendarUrl = ""; + let ordinaryUrl = ""; + let reads = 0; + let writes = 0; + const generations: string[] = []; + const userGenerations: string[] = []; + let expandedScope = false; + let apiUrl: string; + const mcpStatus = (server: InstanceType) => ({ + initializations: server.connectionInitializationCount, + lists: server.toolsListRequestCount, + unauthorized: server.unauthorizedRequestCount, + active: server.activeConnectionCount, + }); + const status = () => ({ + reads, + writes, + generations, + userGenerations, + mail: mcpStatus(mail), + calendar: mcpStatus(calendar), + ordinary: mcpStatus(ordinary), + }); + const plugin = (name: string, url: string) => ({ + name, + logo: "https://images.example/connector.svg", + metadata: { displayName: name, tier: "standard", releaseTag: "preview" }, + connection: { status: "connected" }, + mcpServers: { mcpServers: { [name]: { type: "http", url } } }, + }); + const api = createServer(async (request, response) => { + try { + response.setHeader("content-type", "application/json"); + if (request.url === "/local-test-control") { + if (request.method === "POST") { + let body = ""; + for await (const chunk of request) body += chunk; + const update = JSON.parse(body); + if (update.expandedScope !== undefined) expandedScope = update.expandedScope; + if (update.credential !== undefined) { + const credential = tokens[update.credential as keyof typeof tokens]; + assert(credential); + mail.setRequiredBearerToken(credential); + calendar.setRequiredBearerToken(credential); + } + } + response.end(JSON.stringify(status())); + return; + } + const header = request.headers.authorization ?? ""; + const token = header.slice(header.indexOf(" ") + 1); + const generation = Object.entries(tokens).find(([, value]) => value === token)?.[0] ?? "unknown"; + if (request.url?.startsWith("/copilot_internal/user")) { + userGenerations.push(generation); + if (generation === "unknown") { + response.writeHead(401).end(JSON.stringify({ message: "Bad credentials" })); + } else { + response.end( + JSON.stringify({ + login: + generation === "repository" + ? "repository-user" + : generation === "other" + ? "hubot" + : "octocat", + copilot_plan: "individual_pro", + is_mcp_enabled: true, + endpoints: { api: apiUrl }, + }), + ); + } + return; + } + if (request.url === "/copilot-connectors/api/v1/plugins") { + reads++; + generations.push(generation); + if (expandedScope && generation === "first") { + response + .writeHead(403, { + "x-github-request-id": "LOCAL-CONNECTOR-SCOPE-FIXTURE", + "x-accepted-oauth-scopes": "write:plugin_gateway_connections", + "x-oauth-scopes": "read:user", + }) + .end(JSON.stringify({ message: "insufficient OAuth scope" })); + } else { + response.end( + JSON.stringify({ plugins: [plugin("mail", mailUrl), plugin("calendar", calendarUrl)] }), + ); + } + return; + } + if ( + request.url?.startsWith("/copilot-connectors/api/v1/connectors/managed/") && + ["PUT", "DELETE"].includes(request.method ?? "") + ) { + writes++; + response.end("{}"); + return; + } + response.writeHead(404).end("{}"); + } catch (error) { + console.error("Local fixture request failed", error); + if (!response.headersSent) response.writeHead(500); + response.end(JSON.stringify({ message: "Local fixture failure" })); + } + }); + try { + // Started inside the try so a partial startup failure still reaches the cleanup below, + // and one at a time so no sibling can begin listening after that cleanup has run. + mailUrl = await mail.start(); + calendarUrl = await calendar.start(); + ordinaryUrl = await ordinary.start(); + await new Promise((resolve, reject) => { + api.once("error", reject); + api.listen(0, "127.0.0.1", resolve); + }); + const address = api.address(); + assert(address && typeof address !== "string"); + apiUrl = `http://127.0.0.1:${address.port}`; + await new Promise((resolve, reject) => { + const child = spawn(binary!, [], { + cwd: directory, + stdio: "inherit", + env: { + ...process.env, + HOME: home, + USERPROFILE: home, + COPILOT_HOME: join(directory, "copilot-home"), + COPILOT_DEBUG_GITHUB_API_URL: apiUrl, + COPILOT_API_URL: apiUrl, + COPILOT_GITHUB_TOKEN: tokens.repository, + GH_TOKEN: tokens.repository, + GITHUB_TOKEN: tokens.repository, + COPILOT_SDK_AUTH_TOKEN: "", + NODE_EXTRA_CA_CERTS: ca, + SSL_CERT_FILE: ca, + CURL_CA_BUNDLE: ca, + NO_PROXY: "localhost,127.0.0.1,::1", + no_proxy: "localhost,127.0.0.1,::1", + CONNECTOR_LOCAL_API: apiUrl, + CONNECTOR_LOCAL_DIRECTORY: directory, + CONNECTOR_LOCAL_ORDINARY: ordinaryUrl, + CONNECTOR_LOCAL_CASE: testCase, + CONNECTOR_LOCAL_TRANSPORT: transport, + }, + }); + // Escalate to SIGKILL and only settle once the child is gone, so cleanup never + // removes the working directory or servers an orphan is still using. + let timedOut = false; + let kill: ReturnType | undefined; + const timeout = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + kill = setTimeout(() => child.kill("SIGKILL"), 5_000); + }, CASE_TIMEOUT_MS); + const settle = () => { + clearTimeout(timeout); + if (kill !== undefined) clearTimeout(kill); + }; + child.once("error", (error) => { + settle(); + reject(error); + }); + child.once("exit", (code, signal) => { + settle(); + if (timedOut) { + reject(new Error(`Local integration timeout: ${transport}/${testCase}`)); + return; + } + if (code === 0) resolve(); + else + reject( + new Error(`Local integration failed: ${transport}/${testCase}, exit ${code}, signal ${signal}`), + ); + }); + }); + const report = JSON.stringify({ transport, testCase, ...status() }); + console.log(report); + if (process.env.CONNECTOR_LOCAL_REPORT) { + await appendFile(process.env.CONNECTOR_LOCAL_REPORT, `${report}\n`); + } + } catch (error) { + if (process.env.CONNECTOR_LOCAL_REPORT) { + await appendFile( + process.env.CONNECTOR_LOCAL_REPORT, + `${JSON.stringify({ transport, testCase, passed: false, ...status() })}\n`, + ); + } + throw error; + } finally { + api.closeAllConnections(); + await Promise.all([ + mail.stop(), + calendar.stop(), + ordinary.stop(), + // Closing a server that never listened reports ERR_SERVER_NOT_RUNNING, which would + // mask the startup failure that skipped `api.listen` in the first place. + api.listening + ? new Promise((resolve, reject) => api.close((error) => (error ? reject(error) : resolve()))) + : Promise.resolve(), + ]); + await rm(directory, { recursive: true, force: true }); + } +} + +for (const transport of transports) { + for (const testCase of cases) await runCase(transport, testCase); +} diff --git a/rust/tests/fixtures/connector-runtime/package.json b/rust/tests/fixtures/connector-runtime/package.json new file mode 100644 index 0000000000..e986b24bba --- /dev/null +++ b/rust/tests/fixtures/connector-runtime/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "module" +} diff --git a/rust/tests/fixtures/connector-runtime/src/main.rs b/rust/tests/fixtures/connector-runtime/src/main.rs new file mode 100644 index 0000000000..309a7b18ae --- /dev/null +++ b/rust/tests/fixtures/connector-runtime/src/main.rs @@ -0,0 +1,531 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use github_copilot_sdk::github_token::{ + GitHubToken, GitHubTokenProviderArgs, GitHubTokenProviderResult, GitHubTokenRequestReason, +}; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::rpc::{ + ConnectorAccountRequest, ConnectorAuthorizationScope, ConnectorAvailability, + ConnectorConnectRequest, ConnectorConnectResult, ConnectorMcpStatus, ConnectorReconcileOptions, + ConnectorReconcileRequest, ConnectorSessionAccount, ConnectorStatus, McpDisableRequest, + McpListToolsRequest, +}; +use github_copilot_sdk::session::Session; +use github_copilot_sdk::{ + Client, ClientOptions, LogLevel, McpHttpServerConfig, McpServerConfig, SessionConfig, Transport, +}; +use serde_json::{Value, json}; + +type Result = std::result::Result>; +const FIRST: &str = "ghu_connector_session_first"; +const ROTATED: &str = "ghu_connector_session_rotated"; +const OTHER: &str = "ghu_connector_session_other_user"; +const REPOSITORY: &str = "ghu_connector_repository_identity"; + +fn setting(name: &str) -> String { + std::env::var(name).unwrap_or_else(|_| panic!("Missing test setting {name}")) +} + +fn persistent_config(label: &str) -> Result>> { + let path = PathBuf::from(setting("CONNECTOR_LOCAL_DIRECTORY")) + .join(label) + .join("config.json"); + match std::fs::read(path) { + Ok(contents) => Ok(Some(contents)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(error.into()), + } +} + +async fn control(update: Option) -> Result { + let url = format!("{}/local-test-control", setting("CONNECTOR_LOCAL_API")); + let client = reqwest::Client::new(); + let response = match update { + Some(update) => { + client + .post(url) + .header("content-type", "application/json") + .body(serde_json::to_vec(&update)?) + .send() + .await? + } + None => client.get(url).send().await?, + }; + Ok(serde_json::from_str( + &response.error_for_status()?.text().await?, + )?) +} + +async fn client(label: &str, stdio: bool) -> Result { + let transport = if stdio || setting("CONNECTOR_LOCAL_TRANSPORT") == "stdio" { + Transport::Stdio + } else { + Transport::InProcess + }; + Ok(Client::start( + ClientOptions::new() + .with_transport(transport) + .with_log_level(LogLevel::Error) + .with_use_logged_in_user(false) + .with_base_directory(PathBuf::from(setting("CONNECTOR_LOCAL_DIRECTORY")).join(label)), + ) + .await?) +} + +fn config(enabled: bool) -> SessionConfig { + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_feature_flags(HashMap::from([ + ("MANAGED_MCP_SERVERS".to_owned(), enabled), + ("CONNECTORS".to_owned(), true), + ])) + .with_request_extensions(false) + .with_disabled_mcp_servers(["github-mcp-server"]) + .with_working_directory(setting("CONNECTOR_LOCAL_DIRECTORY")) + .with_mcp_servers( + [( + "ordinary".to_owned(), + McpServerConfig::Http(McpHttpServerConfig { + url: setting("CONNECTOR_LOCAL_ORDINARY"), + tools: Some(vec!["*".to_owned()]), + ..Default::default() + }), + )] + .into_iter() + .collect(), + ) +} + +async fn account(session: &Session) -> Result { + let capabilities = session.rpc().connectors().get_capabilities().await?; + assert_eq!(capabilities.availability, ConnectorAvailability::Enabled); + assert_eq!(capabilities.session_account_selection, Some(true)); + assert_eq!(capabilities.targeted_reconcile, Some(true)); + let before = control(None).await?; + let selected = session + .rpc() + .connectors() + .get_account() + .await? + .ok_or("Expected an eligible session account")?; + let after = control(None).await?; + assert_eq!(before["reads"], after["reads"]); + assert_eq!(selected.auth_info.login, "octocat"); + assert_eq!(selected.auth_info.host, "https://github.com"); + let public = serde_json::to_value(&selected)?; + assert_eq!( + public + .as_object() + .expect("serialized account is a JSON object") + .len(), + 2 + ); + assert_eq!( + public["authInfo"] + .as_object() + .expect("authInfo is a JSON object") + .len(), + 3 + ); + assert_no_secrets(&public); + Ok(selected) +} + +fn assert_no_secrets(value: &Value) { + let text = serde_json::to_string(value).expect("value serializes to JSON"); + for forbidden in [ + FIRST, + ROTATED, + OTHER, + REPOSITORY, + "registrationId", + "accessToken", + "127.0.0.1", + ] { + assert!( + !text.contains(forbidden), + "Public Connector state leaked a private value" + ); + } +} + +async fn reconcile( + session: &Session, + account_id: &str, + force: Option<&str>, +) -> Result { + let connectors = session.rpc().connectors(); + Ok(match force { + Some(force) => { + connectors + .reconcile_with_options( + ConnectorReconcileOptions::new(account_id) + .refresh_catalog(true) + .force_connector_name(force), + ) + .await? + } + None => { + connectors + .reconcile(ConnectorReconcileRequest { + account_id: account_id.to_owned(), + refresh_catalog: Some(true), + }) + .await? + } + }) +} + +fn server_id(status: &ConnectorStatus, name: &str) -> String { + status + .runtime_servers + .iter() + .find(|server| server.connector_name == name) + .unwrap_or_else(|| panic!("Missing projected Connector {name}")) + .runtime_server_id + .clone() +} + +fn check_connected(status: &ConnectorStatus) { + assert!( + status + .runtime_servers + .iter() + .all(|server| server.status == ConnectorMcpStatus::Connected) + ); + assert!( + status + .runtime_servers + .iter() + .any(|server| server.connector_name == "mail") + ); + let catalog = status.catalog.as_ref().expect("catalog"); + assert_eq!(catalog.connectors[0].tier.as_deref(), Some("standard")); + assert_eq!( + catalog.connectors[0].release_tag.as_deref(), + Some("preview") + ); + assert!(catalog.connectors[0].logo.is_some()); + assert_no_secrets(&serde_json::to_value(status).expect("status serializes to JSON")); +} + +async fn static_identity() -> Result<()> { + let client = client("static", false).await?; + let config_before = persistent_config("static")?; + let accounts_before = serde_json::to_value(client.rpc().account().get_all_users().await?)?; + let session = client + .create_session(config(true).with_github_token(FIRST)) + .await?; + let selected = account(&session).await?; + assert!( + session + .rpc() + .connectors() + .get_status() + .await? + .account_id + .is_none() + ); + let second = client + .create_session(config(true).with_github_token(FIRST)) + .await?; + let other = account(&second).await?; + assert_ne!(selected.account_id, other.account_id); + let before = control(None).await?; + assert!( + second + .rpc() + .connectors() + .refresh(ConnectorAccountRequest { + account_id: selected.account_id.clone() + }) + .await + .is_err() + ); + assert_eq!(before["reads"], control(None).await?["reads"]); + let status = reconcile(&session, &selected.account_id, None).await?; + check_connected(&status); + assert_eq!(control(None).await?["generations"], json!(["first"])); + assert_eq!(control(None).await?["writes"], json!(0)); + assert_eq!( + serde_json::to_value(client.rpc().account().get_all_users().await?)?, + accounts_before + ); + second.disconnect().await?; + session.disconnect().await?; + client.stop().await?; + assert!( + persistent_config("static")? == config_before, + "Session credentials changed persistent account configuration" + ); + Ok(()) +} + +async fn provider_case(scope: bool) -> Result<()> { + if scope { + control(Some(json!({ "expandedScope": true }))).await?; + } + let mode = Arc::new(AtomicUsize::new(0)); + let reasons = Arc::new(Mutex::new(Vec::new())); + let provider = { + let mode = mode.clone(); + let reasons = reasons.clone(); + Arc::new(move |args: GitHubTokenProviderArgs| { + let mode = mode.clone(); + let reasons = reasons.clone(); + async move { + reasons + .lock() + .expect("token reason lock is not poisoned") + .push(args.reason); + let token = match mode.load(Ordering::SeqCst) { + 0 => FIRST, + 1 => ROTATED, + _ => OTHER, + }; + Ok(GitHubTokenProviderResult::Token(GitHubToken::new( + token, 28_800, + ))) + } + }) + }; + let client = client("provider", false).await?; + let config_before = persistent_config("provider")?; + let accounts_before = serde_json::to_value(client.rpc().account().get_all_users().await?)?; + let session = client + .create_session(config(true).with_github_token_provider(provider)) + .await?; + let selected = account(&session).await?; + if scope { + assert!( + session + .rpc() + .connectors() + .refresh(ConnectorAccountRequest { + account_id: selected.account_id.clone(), + }) + .await + .is_err() + ); + let blocked = session.rpc().connectors().get_status().await?; + let requirement = blocked + .authorization_requirement + .ok_or("Missing scope requirement")?; + assert_eq!(requirement.account_id, selected.account_id); + assert_eq!( + requirement.scope, + ConnectorAuthorizationScope::WritePluginGatewayConnections + ); + assert_eq!( + *reasons.lock().expect("token reason lock is not poisoned"), + vec![GitHubTokenRequestReason::Initial] + ); + mode.store(1, Ordering::SeqCst); + control(Some(json!({ "credential": "rotated" }))).await?; + let recovered = reconcile(&session, &selected.account_id, Some("mail")).await?; + check_connected(&recovered); + assert!(recovered.authorization_requirement.is_none()); + assert_eq!( + recovered.account_id.as_deref(), + Some(selected.account_id.as_str()) + ); + assert_eq!( + control(None).await?["generations"], + json!(["first", "rotated"]) + ); + assert_eq!( + *reasons.lock().expect("token reason lock is not poisoned"), + vec![ + GitHubTokenRequestReason::Initial, + GitHubTokenRequestReason::Refresh + ] + ); + } else { + let status = reconcile(&session, &selected.account_id, None).await?; + check_connected(&status); + let mail = server_id(&status, "mail"); + let before = control(None).await?; + mode.store(1, Ordering::SeqCst); + control(Some(json!({ "credential": "rotated" }))).await?; + let tools = session + .rpc() + .mcp() + .list_tools(McpListToolsRequest { + server_name: mail.clone(), + }) + .await?; + assert!(tools.tools.iter().any(|tool| tool.name == "remote_ping")); + assert_eq!(account(&session).await?.account_id, selected.account_id); + let after = control(None).await?; + assert_eq!( + after["mail"]["initializations"], + before["mail"]["initializations"] + ); + assert!( + after["mail"]["unauthorized"] + .as_u64() + .expect("mail unauthorized count is a number") + > 0 + ); + assert_eq!( + *reasons.lock().expect("token reason lock is not poisoned"), + vec![ + GitHubTokenRequestReason::Initial, + GitHubTokenRequestReason::Refresh + ] + ); + mode.store(2, Ordering::SeqCst); + control(Some(json!({ "credential": "other" }))).await?; + let successful_lists = after["mail"]["lists"].clone(); + assert!( + session + .rpc() + .mcp() + .list_tools(McpListToolsRequest { server_name: mail }) + .await + .is_err() + ); + assert_eq!(control(None).await?["mail"]["lists"], successful_lists); + assert_eq!(account(&session).await?.auth_info.login, "octocat"); + } + assert_eq!(control(None).await?["writes"], json!(0)); + assert_eq!( + serde_json::to_value(client.rpc().account().get_all_users().await?)?, + accounts_before + ); + session.disconnect().await?; + client.stop().await?; + assert!( + persistent_config("provider")? == config_before, + "Session callback changed persistent account configuration" + ); + Ok(()) +} + +async fn targeted() -> Result<()> { + let first_client = client("first-process", true).await?; + let second_client = client("second-process", false).await?; + let first = first_client + .create_session(config(true).with_github_token(FIRST)) + .await?; + let second = second_client + .create_session(config(true).with_github_token(FIRST)) + .await?; + let first_account = account(&first).await?; + let second_account = account(&second).await?; + reconcile(&first, &first_account.account_id, None).await?; + reconcile(&second, &second_account.account_id, None).await?; + let outcome = first + .rpc() + .connectors() + .reconnect(ConnectorConnectRequest { + account_id: first_account.account_id, + connector_name: "mail".to_owned(), + }) + .await?; + assert!(matches!(outcome, ConnectorConnectResult::Connected(_))); + assert_eq!(control(None).await?["writes"], json!(1)); + let before = control(None).await?; + let status = reconcile(&second, &second_account.account_id, Some("mail")).await?; + check_connected(&status); + let after = control(None).await?; + assert_eq!( + after["mail"]["initializations"] + .as_u64() + .expect("mail initializations count is a number"), + before["mail"]["initializations"] + .as_u64() + .expect("mail initializations count is a number") + + 1 + ); + assert_eq!( + after["calendar"]["initializations"], + before["calendar"]["initializations"] + ); + assert_eq!( + after["ordinary"]["initializations"], + before["ordinary"]["initializations"] + ); + assert_eq!(after["writes"], json!(1)); + second + .rpc() + .mcp() + .disable(McpDisableRequest { + server_name: server_id(&status, "calendar"), + }) + .await?; + let status = reconcile(&second, &second_account.account_id, Some("mail")).await?; + assert!( + status + .runtime_servers + .iter() + .any(|server| server.connector_name == "calendar" + && server.status == ConnectorMcpStatus::Disabled) + ); + let final_state = control(None).await?; + assert_eq!( + final_state["calendar"]["initializations"], + before["calendar"]["initializations"] + ); + assert_eq!( + final_state["ordinary"]["initializations"], + before["ordinary"]["initializations"] + ); + assert_eq!(final_state["writes"], json!(1)); + first.disconnect().await?; + second.disconnect().await?; + first_client.stop().await?; + second_client.stop().await?; + Ok(()) +} + +async fn disabled() -> Result<()> { + let client = client("disabled", false).await?; + let session = client + .create_session(config(false).with_github_token(FIRST)) + .await?; + let capabilities = session.rpc().connectors().get_capabilities().await?; + assert_eq!(capabilities.availability, ConnectorAvailability::Disabled); + assert_eq!(capabilities.session_account_selection, Some(true)); + assert_eq!(capabilities.targeted_reconcile, Some(true)); + let before = control(None).await?; + assert!(session.rpc().connectors().get_account().await?.is_none()); + reconcile(&session, "not-resolved", Some("mail")).await?; + let after = control(None).await?; + for key in ["reads", "writes", "mail", "calendar"] { + assert_eq!(after[key], before[key]); + } + session.disconnect().await?; + client.stop().await?; + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let case = setting("CONNECTOR_LOCAL_CASE"); + let work = async { + match case.as_str() { + "static" => static_identity().await, + "rotation" => provider_case(false).await, + "scope" => provider_case(true).await, + "targeted" => targeted().await, + "disabled" => disabled().await, + _ => Err("Unknown local integration case".into()), + } + }; + tokio::time::timeout(Duration::from_secs(180), work).await??; + println!( + "PASS Rust SDK {} {}", + setting("CONNECTOR_LOCAL_TRANSPORT"), + case + ); + Ok(()) +} diff --git a/rust/tests/fixtures/connector-runtime/vitest.local.config.ts b/rust/tests/fixtures/connector-runtime/vitest.local.config.ts new file mode 100644 index 0000000000..26f85647e6 --- /dev/null +++ b/rust/tests/fixtures/connector-runtime/vitest.local.config.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// Local-only by design: this matrix is not referenced by any repository vitest +// config or workflow. + +import { fileURLToPath } from "node:url"; + +import { matrixTimeoutMs } from "./budget.ts"; + +export default { + root: fileURLToPath(new URL("../../../../", import.meta.url)), + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), + test: { + include: ["rust/tests/fixtures/connector-runtime/fixture.test.ts"], + pool: "forks", + testTimeout: matrixTimeoutMs, + }, +}; diff --git a/rust/tests/mcp_prompts_test.rs b/rust/tests/mcp_prompts_test.rs new file mode 100644 index 0000000000..2fd54d89e5 --- /dev/null +++ b/rust/tests/mcp_prompts_test.rs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![allow(clippy::unwrap_used)] + +use std::path::Path; + +use github_copilot_sdk::rpc::{ + McpPromptsGetRequest, McpPromptsGetResult, McpPromptsListRequest, McpPromptsListResult, +}; +use serde_json::{Value, json}; + +// Serialization coverage complements Node/.NET's real MCP boundary tests. +#[test] +fn prompt_results_preserve_opaque_content_and_optional_fields() { + let fixture_path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../test/harness/mcp-prompt-fixtures.json"); + let fixtures: Value = + serde_json::from_str(&std::fs::read_to_string(fixture_path).unwrap()).unwrap(); + for page in ["firstPage", "secondPage"] { + let result: McpPromptsListResult = serde_json::from_value(fixtures[page].clone()).unwrap(); + assert_eq!(serde_json::to_value(result).unwrap(), fixtures[page]); + } + let result: McpPromptsGetResult = + serde_json::from_value(fixtures["richPrompt"].clone()).unwrap(); + assert_eq!( + serde_json::to_value(result).unwrap(), + fixtures["richPrompt"] + ); +} + +#[test] +fn prompt_requests_preserve_omitted_empty_and_string_arguments() { + for arguments in [ + None, + Some(json!({})), + Some(json!({"topic": "日本語", "style": ""})), + ] { + let mut wire = json!({"serverName": "fixture", "promptName": "rich"}); + if let Some(arguments) = arguments { + wire["arguments"] = arguments; + } + let request: McpPromptsGetRequest = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(request).unwrap(), wire); + } + for wire in [ + json!({"serverName": "fixture"}), + json!({"serverName": "fixture", "cursor": "page:2/opaque+cursor="}), + ] { + let request: McpPromptsListRequest = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(request).unwrap(), wire); + } + assert!( + serde_json::from_value::( + json!({"serverName": "fixture", "promptName": "rich", "arguments": {"topic": 42}}) + ) + .is_err() + ); +} diff --git a/rust/tests/session_events_test.rs b/rust/tests/session_events_test.rs index 401fa0a09c..9ccb5c833c 100644 --- a/rust/tests/session_events_test.rs +++ b/rust/tests/session_events_test.rs @@ -3,11 +3,28 @@ #![allow(clippy::unwrap_used)] use github_copilot_sdk::session_events::{ - IndexedSearchData, PermissionApprovalEvaluation, PermissionApprovalEvaluationEvaluationStage, - PermissionApprovalEvaluationJudgeStatus, PermissionApprovalEvaluationReasonCode, - SandboxDecisionData, SessionEventData, TypedSessionEvent, UserMessageData, + HumanResponseRecordedResponse, IndexedSearchData, PermissionApprovalEvaluation, + PermissionApprovalEvaluationEvaluationStage, PermissionApprovalEvaluationJudgeStatus, + PermissionApprovalEvaluationReasonCode, SandboxDecisionData, SessionEventData, + ToolExecutionCompleteFileEdit, ToolExecutionCompleteFileEditKind, TypedSessionEvent, + UserMessageData, }; +#[test] +fn future_file_edit_kind_retains_its_wire_value() { + let wire = serde_json::json!({ "path": "/future.txt", "kind": "append" }); + let edit: ToolExecutionCompleteFileEdit = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!( + edit.kind, + ToolExecutionCompleteFileEditKind::Unknown("append".to_owned()) + ); + assert_eq!(serde_json::to_value(edit).unwrap(), wire); + assert_eq!( + serde_json::to_value(ToolExecutionCompleteFileEditKind::Create).unwrap(), + "create" + ); +} + fn event_envelope(event_type: &str, data: serde_json::Value) -> serde_json::Value { serde_json::json!({ "id": "10000000-0000-4000-8000-000000000001", @@ -217,6 +234,90 @@ fn ordinary_and_empty_payloads_round_trip_full_event_envelopes() { } } +#[test] +fn human_response_variants_round_trip_as_typed_event_data() { + let payloads = [ + serde_json::json!({ + "requestId": "ask-user-request", + "toolCallId": "ask-user-tool", + "actor": "human_response", + "response": { + "responseKind": "ask_user", + "message": "Which repository?", + "requestedSchema": { + "type": "object", + "properties": { + "answer": { + "type": "string" + } + } + }, + "content": { + "answer": "github/st-mcp-poc" + } + } + }), + serde_json::json!({ + "requestId": "question-request", + "toolCallId": "question-tool", + "actor": "human_response", + "response": { + "responseKind": "user_input", + "question": "Which file?", + "choices": ["alpha.txt", "beta.txt"], + "allowFreeform": true, + "answer": "alpha.txt", + "wasFreeform": false + } + }), + serde_json::json!({ + "requestId": "plan-request", + "toolCallId": "plan-tool", + "actor": "human_response", + "response": { + "responseKind": "exit_plan_mode", + "summary": "Edit alpha.txt", + "planContent": "Edit alpha.txt and run its test.", + "actions": ["autopilot", "interactive", "exit_only"], + "recommendedAction": "interactive", + "approved": true, + "selectedAction": "interactive", + "autoApproveEdits": false + } + }), + ]; + + for wire in payloads.map(|payload| event_envelope("human_response.recorded", payload)) { + let event: TypedSessionEvent = serde_json::from_value(wire.clone()).unwrap(); + let SessionEventData::HumanResponseRecorded(data) = &event.payload else { + panic!("expected human-response event"); + }; + let response_kind = match &data.response { + HumanResponseRecordedResponse::AskUser(response) => { + assert_eq!( + response.content.get("answer"), + Some(&serde_json::json!("github/st-mcp-poc")) + ); + "ask_user" + } + HumanResponseRecordedResponse::UserInput(response) => { + assert_eq!(response.answer, "alpha.txt"); + "user_input" + } + HumanResponseRecordedResponse::ExitPlanMode(response) => { + assert!(response.approved); + "exit_plan_mode" + } + }; + + assert_eq!( + response_kind, + wire["data"]["response"]["responseKind"].as_str().unwrap() + ); + assert_eq!(serde_json::to_value(event).unwrap(), wire); + } +} + #[test] fn approval_evaluation_preserves_protocol_unknown_values() { let wire = serde_json::json!({ diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 73e0b7cd4d..f8edcc3fe6 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -18,11 +18,12 @@ use github_copilot_sdk::handler::{ PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, }; use github_copilot_sdk::rpc::{ - CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - ConnectorAccountRequest, ConnectorAvailability, ConnectorCapabilities, ConnectorCatalogResult, - ConnectorCatalogStatus, ConnectorConnectRequest, ConnectorConnectResult, - ConnectorContinueRequest, ConnectorDisconnectResult, ConnectorMcpStatus, - ConnectorReconcileRequest, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance, + AuthInfoType, CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, + CanvasProviderOpenResult, ConnectorAccountRequest, ConnectorAvailability, + ConnectorCapabilities, ConnectorCatalogResult, ConnectorCatalogStatus, ConnectorConnectRequest, + ConnectorConnectResult, ConnectorContinueRequest, ConnectorDisconnectResult, + ConnectorMcpStatus, ConnectorReconcileOptions, ConnectorReconcileRequest, + ConnectorSessionAccount, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance, SendAgentMode, SendMode, SendRequest, SessionRpcConnectors, }; use github_copilot_sdk::session_events::{ @@ -1796,7 +1797,8 @@ async fn create_and_resume_send_managed_settings_permissions() { .with_disable_bypass_permissions_mode(DisableBypassPermissionsModes::ALLOW_AUTO_ONLY) .with_deny(vec!["shell(rm*)".to_string()]) .with_ask(vec!["write".to_string()]) - .with_allow(vec![]), + .with_allow(vec![]) + .with_limit_to(vec![]), ); let create_handle = tokio::spawn({ @@ -1822,6 +1824,7 @@ async fn create_and_resume_send_managed_settings_permissions() { assert_eq!(perms["deny"][0], "shell(rm*)"); assert_eq!(perms["ask"][0], "write"); assert_eq!(perms["allow"], serde_json::json!([])); + assert_eq!(perms["limitTo"], serde_json::json!([])); let id = request["id"].as_u64().unwrap(); let session_id = requested_session_id(&request).to_string(); @@ -5373,7 +5376,8 @@ async fn nested_handler_panics_still_send_cancellation_replies() { _request_id: RequestId, _request: ElicitationRequest, ) -> ElicitationResult { - panic!("test elicitation handler panic"); + // Test cancellation after unwinding, not progress of the panic diagnostic sink. + std::panic::resume_unwind(Box::new("test elicitation handler panic")); } } @@ -5385,7 +5389,7 @@ async fn nested_handler_panics_still_send_cancellation_replies() { _request_id: RequestId, _request: McpAuthRequest, ) -> McpAuthResult { - panic!("test MCP-auth handler panic"); + std::panic::resume_unwind(Box::new("test MCP-auth handler panic")); } } @@ -5759,6 +5763,448 @@ async fn external_tool_broadcast_for_unknown_tool_is_not_responded_to() { ); } +/// Answers every call with its label, so a test can tell which handler served it. +struct LabelTool(&'static str); + +#[async_trait] +impl tool::ToolHandler for LabelTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + Ok(ToolResult::Text(self.0.to_string())) + } +} + +fn label_tool(name: &str, label: &'static str) -> Tool { + Tool::new(name) + .with_description(format!("{name} tool")) + .with_parameters(serde_json::json!({"type": "object"})) + .with_handler(Arc::new(LabelTool(label))) +} + +async fn request_external_tool(server: &mut FakeServer, request_id: &str, tool_name: &str) { + server + .send_event( + "external_tool.requested", + serde_json::json!({ + "requestId": request_id, + "sessionId": server.session_id, + "toolCallId": format!("call-{request_id}"), + "toolName": tool_name, + "arguments": {}, + }), + ) + .await; +} + +/// Reads the handler's answer to `request_id`. +async fn read_tool_result(server: &mut FakeServer, request_id: &str) -> Value { + let reply = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(reply["method"], "session.tools.handlePendingToolCall"); + assert_eq!(reply["params"]["requestId"], request_id); + reply["params"]["result"].clone() +} + +async fn assert_no_request(server: &mut FakeServer) { + let request = timeout(Duration::from_millis(150), server.read_request()).await; + assert!(request.is_err(), "unexpected request: {:?}", request.ok()); +} + +#[tokio::test] +async fn set_tools_replaces_the_definitions_and_handlers_together() { + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![label_tool("old", "old handler")]) + }) + .await; + let session = Arc::new(session); + + let replace = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([ + label_tool("new", "new handler"), + Tool::new("declared").with_description("Declared only"), + ]) + .await + } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.tools.set"); + assert_eq!( + request["params"], + serde_json::json!({ + "sessionId": server.session_id, + "tools": [ + { "name": "new", "description": "new tool", "parameters": { "type": "object" } }, + { "name": "declared", "description": "Declared only" }, + ], + }) + ); + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, replace).await.unwrap().unwrap().unwrap(); + + request_external_tool(&mut server, "req-new", "new").await; + assert_eq!( + read_tool_result(&mut server, "req-new").await, + "new handler" + ); + + // Neither the removed tool nor the declaration-only tool is answered here. + request_external_tool(&mut server, "req-old", "old").await; + request_external_tool(&mut server, "req-declared", "declared").await; + assert_no_request(&mut server).await; +} + +// With one worker, the event loop usually dispatches the next message before +// the replacement task resumes, so this exercises the swap on the read task. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn set_tools_keeps_the_previous_handlers_until_the_runtime_accepts_the_replacement() { + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![label_tool("old", "old handler")]) + }) + .await; + let session = Arc::new(session); + + let replace = tokio::spawn({ + let session = session.clone(); + async move { session.set_tools([label_tool("new", "new handler")]).await } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.tools.set"); + + // Until the runtime accepts, the outgoing tool is still this client's, and + // the incoming one may still belong to another client. + request_external_tool(&mut server, "req-old", "old").await; + assert_eq!( + read_tool_result(&mut server, "req-old").await, + "old handler" + ); + request_external_tool(&mut server, "req-new", "new").await; + assert_no_request(&mut server).await; + + // The first messages after the response already see exactly the accepted + // set: the removed tool is not answered, and the added one is. + server.respond(&request, serde_json::json!({})).await; + request_external_tool(&mut server, "req-old-after", "old").await; + request_external_tool(&mut server, "req-new-after", "new").await; + assert_eq!( + read_tool_result(&mut server, "req-new-after").await, + "new handler" + ); + assert_no_request(&mut server).await; + timeout(TIMEOUT, replace).await.unwrap().unwrap().unwrap(); +} + +#[tokio::test] +async fn set_tools_lets_a_running_call_finish_on_its_original_handler() { + /// Blocks each call until released, then answers with its label. + struct GatedTool { + started: parking_lot::Mutex>>, + release: Arc, + } + + #[async_trait] + impl tool::ToolHandler for GatedTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + if let Some(started) = self.started.lock().take() { + let _ = started.send(()); + } + self.release.notified().await; + Ok(ToolResult::Text("old lookup".to_string())) + } + } + + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let release = Arc::new(Notify::new()); + let gated = Tool::new("lookup") + .with_description("lookup tool") + .with_parameters(serde_json::json!({"type": "object"})) + .with_handler(Arc::new(GatedTool { + started: parking_lot::Mutex::new(Some(started_tx)), + release: release.clone(), + })); + let (session, mut server) = + create_session_pair_with_config(move |cfg| cfg.with_tools(vec![gated])).await; + let session = Arc::new(session); + + request_external_tool(&mut server, "req-running", "lookup").await; + timeout(TIMEOUT, started_rx).await.unwrap().unwrap(); + + let replace = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([label_tool("lookup", "new lookup")]) + .await + } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.tools.set"); + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, replace).await.unwrap().unwrap().unwrap(); + + request_external_tool(&mut server, "req-after", "lookup").await; + assert_eq!( + read_tool_result(&mut server, "req-after").await, + "new lookup" + ); + + release.notify_one(); + assert_eq!( + read_tool_result(&mut server, "req-running").await, + "old lookup" + ); +} + +#[tokio::test] +async fn set_tools_leaves_the_handlers_unchanged_when_the_runtime_rejects_it() { + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![label_tool("old", "old handler")]) + }) + .await; + let session = Arc::new(session); + + let replace = tokio::spawn({ + let session = session.clone(); + async move { session.set_tools([label_tool("new", "new handler")]).await } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.tools.set"); + + // Another client owns `new`, so its calls are never this client's to answer. + request_external_tool(&mut server, "req-new", "new").await; + assert_no_request(&mut server).await; + server + .respond_error( + &request, + -32602, + "External tool name clash: new already registered by another connection", + ) + .await; + let error = timeout(TIMEOUT, replace) + .await + .unwrap() + .unwrap() + .unwrap_err(); + assert!( + matches!(error.kind(), ErrorKind::Rpc { code: -32602 }), + "{error}" + ); + + request_external_tool(&mut server, "req-new-after", "new").await; + assert_no_request(&mut server).await; + request_external_tool(&mut server, "req-old", "old").await; + assert_eq!( + read_tool_result(&mut server, "req-old").await, + "old handler" + ); +} + +#[tokio::test] +async fn set_tools_completes_the_replacement_when_the_caller_stops_waiting() { + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![label_tool("old", "old handler")]) + }) + .await; + let session = Arc::new(session); + + let abandoned = tokio::spawn({ + let session = session.clone(); + async move { session.set_tools([label_tool("new", "new handler")]).await } + }); + let abandoned_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(abandoned_request["method"], "session.tools.set"); + abandoned.abort(); + assert!(abandoned.await.unwrap_err().is_cancelled()); + + // A later replacement still waits for the abandoned one. + let next = tokio::spawn({ + let session = session.clone(); + async move { session.set_tools(Vec::new()).await } + }); + assert_no_request(&mut server).await; + server + .respond(&abandoned_request, serde_json::json!({})) + .await; + let next_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(next_request["params"]["tools"], serde_json::json!([])); + + // The abandoned replacement still took effect. + request_external_tool(&mut server, "req-old", "old").await; + request_external_tool(&mut server, "req-new", "new").await; + assert_eq!( + read_tool_result(&mut server, "req-new").await, + "new handler" + ); + assert_no_request(&mut server).await; + + server.respond(&next_request, serde_json::json!({})).await; + timeout(TIMEOUT, next).await.unwrap().unwrap().unwrap(); + request_external_tool(&mut server, "req-new-after", "new").await; + assert_no_request(&mut server).await; +} + +#[tokio::test] +async fn set_tools_rejects_duplicate_handlers_without_contacting_the_runtime() { + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![label_tool("old", "old handler")]) + }) + .await; + let session = Arc::new(session); + + let error = session + .set_tools([label_tool("dup", "first"), label_tool("dup", "second")]) + .await + .unwrap_err(); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig), "{error}"); + + // The next request on the wire is this probe, so nothing was sent before it. + let probe = tokio::spawn({ + let session = session.clone(); + async move { session.abort().await } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.abort"); + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, probe).await.unwrap().unwrap().unwrap(); + + request_external_tool(&mut server, "req-old", "old").await; + assert_eq!( + read_tool_result(&mut server, "req-old").await, + "old handler" + ); +} + +#[tokio::test] +async fn set_tools_applies_concurrent_replacements_in_order() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + let first = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([label_tool("first", "first handler")]) + .await + } + }); + let first_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(first_request["params"]["tools"][0]["name"], "first"); + + let second = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([label_tool("second", "second handler")]) + .await + } + }); + // The second replacement waits for the runtime to answer the first. + assert_no_request(&mut server).await; + server.respond(&first_request, serde_json::json!({})).await; + timeout(TIMEOUT, first).await.unwrap().unwrap().unwrap(); + + let second_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(second_request["params"]["tools"][0]["name"], "second"); + server.respond(&second_request, serde_json::json!({})).await; + timeout(TIMEOUT, second).await.unwrap().unwrap().unwrap(); + + request_external_tool(&mut server, "req-first", "first").await; + assert_no_request(&mut server).await; + request_external_tool(&mut server, "req-second", "second").await; + assert_eq!( + read_tool_result(&mut server, "req-second").await, + "second handler" + ); +} + +#[tokio::test] +async fn set_tools_sends_nothing_when_dropped_while_queued() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + let first = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([label_tool("first", "first handler")]) + .await + } + }); + let first_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + + let queued = tokio::spawn({ + let session = session.clone(); + async move { + session + .set_tools([label_tool("queued", "queued handler")]) + .await + } + }); + assert_no_request(&mut server).await; + queued.abort(); + assert!(queued.await.unwrap_err().is_cancelled()); + + server.respond(&first_request, serde_json::json!({})).await; + timeout(TIMEOUT, first).await.unwrap().unwrap().unwrap(); + assert_no_request(&mut server).await; + + request_external_tool(&mut server, "req-queued", "queued").await; + assert_no_request(&mut server).await; + request_external_tool(&mut server, "req-first", "first").await; + assert_eq!( + read_tool_result(&mut server, "req-first").await, + "first handler" + ); +} + +#[tokio::test] +async fn set_tools_replaces_the_tools_of_a_resumed_session() { + use github_copilot_sdk::types::ResumeSessionConfig; + + let (client, server_read, server_write) = make_client(); + let mut server = FakeServer { + read: server_read, + write: server_write, + session_id: "resumed-with-tools".to_string(), + }; + let resume = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::from("resumed-with-tools")) + .with_tools(vec![label_tool("old", "old handler")]), + ) + .await + .unwrap() + } + }); + let resume_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(resume_request["method"], "session.resume"); + server_respond_create(&mut server.write, &resume_request, "resumed-with-tools").await; + respond_to_reload(&mut server.read, &mut server.write).await; + let session = Arc::new(timeout(TIMEOUT, resume).await.unwrap().unwrap()); + + let replace = tokio::spawn({ + let session = session.clone(); + async move { session.set_tools(Vec::new()).await } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.tools.set"); + assert_eq!(request["params"]["tools"], serde_json::json!([])); + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, replace).await.unwrap().unwrap().unwrap(); + + request_external_tool(&mut server, "req-old", "old").await; + assert_no_request(&mut server).await; +} + #[tokio::test] async fn permission_broadcast_with_resolved_by_hook_is_not_responded_to() { // Phase H: when the runtime marks a permission request as already @@ -6458,6 +6904,90 @@ async fn hooks_invoke_returns_empty_for_unregistered_hook() { assert_eq!(response["result"]["output"], serde_json::json!({})); } +#[tokio::test] +async fn hooks_invoke_subagent_lifecycle_does_not_warn_as_unknown() { + use github_copilot_sdk::hooks::SessionHooks; + + struct EmptyHooks; + #[async_trait] + impl SessionHooks for EmptyHooks {} + + let (capture, _guard) = capture_traces(); + let (_session, mut server) = create_session_pair_with_hooks(Arc::new(EmptyHooks)).await; + + for (request_id, hook_type, input) in [ + ( + 302, + "subagentStart", + serde_json::json!({ + "sessionId": server.session_id, + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "task" + }), + ), + ( + 303, + "subagentStop", + serde_json::json!({ + "sessionId": server.session_id, + "timestamp": 1234567890, + "cwd": "/tmp", + "transcriptPath": "/tmp/transcript.jsonl", + "agentName": "task", + "agentType": "task", + "stopReason": "end_turn", + "response": "done" + }), + ), + ] { + server + .send_request( + request_id, + "hooks.invoke", + serde_json::json!({ + "sessionId": server.session_id, + "hookType": hook_type, + "input": input + }), + ) + .await; + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], request_id); + assert_eq!(response["result"], serde_json::json!({ "output": {} })); + } + + server + .send_request( + 304, + "hooks.invoke", + serde_json::json!({ + "sessionId": server.session_id, + "hookType": "unrecognizedHook", + "input": {} + }), + ) + .await; + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["result"], serde_json::json!({ "output": {} })); + + let warnings: Vec<_> = capture + .events + .lock() + .unwrap() + .iter() + .filter(|event| event.message_contains("unknown hook type")) + .cloned() + .collect(); + assert_eq!(warnings.len(), 1, "unknown hook warnings: {warnings:?}"); + assert!( + warnings[0].field_is("hook_type", "unrecognizedHook"), + "unexpected hook warning: {:?}", + warnings[0] + ); +} + async fn create_session_pair_with_system_message_transforms( transforms: Arc, ) -> (github_copilot_sdk::session::Session, FakeServer) { @@ -6655,7 +7185,9 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { "maxDeadlineMs": 60_000, "maxPollAttempts": 10, "maxPollIntervalMs": 5_000, - "opaqueAccountSelection": true + "opaqueAccountSelection": true, + "sessionAccountSelection": true, + "targetedReconcile": true }); let server_handle = tokio::spawn(async move { @@ -6665,6 +7197,19 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { serde_json::json!({ "sessionId": session_id }), capabilities, ), + ( + "session.connectors.getAccount", + serde_json::json!({ "sessionId": session_id }), + serde_json::json!({ + "accountId": "account-1", + "authInfo": { "type": "token", "host": "github.com", "login": "alice" }, + }), + ), + ( + "session.connectors.getAccount", + serde_json::json!({ "sessionId": session_id }), + serde_json::Value::Null, + ), ( "session.connectors.getStatus", serde_json::json!({ "sessionId": session_id }), @@ -6744,6 +7289,15 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { "refreshCatalog": true, "sessionId": session_id }), + status.clone(), + ), + ( + "session.connectors.reconcile", + serde_json::json!({ + "accountId": "account-1", + "forceConnectorName": "github", + "sessionId": session_id + }), status, ), ]; @@ -6761,6 +7315,16 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { let capabilities: ConnectorCapabilities = connectors.get_capabilities().await.unwrap(); assert_eq!(capabilities.availability, ConnectorAvailability::Enabled); assert_eq!(capabilities.max_poll_attempts, 10); + assert_eq!(capabilities.session_account_selection, Some(true)); + assert_eq!(capabilities.targeted_reconcile, Some(true)); + + let account: Option = connectors.get_account().await.unwrap(); + let account = account.unwrap(); + assert_eq!(account.account_id, "account-1"); + assert_eq!(account.auth_info.r#type, AuthInfoType::Token); + assert_eq!(account.auth_info.host, "github.com"); + assert_eq!(account.auth_info.login, "alice"); + assert!(connectors.get_account().await.unwrap().is_none()); let status: ConnectorStatus = connectors.get_status().await.unwrap(); assert_eq!(status.account_id.as_deref(), Some("account-1")); @@ -6847,6 +7411,14 @@ async fn rpc_namespace_session_connectors_dispatches_all_methods() { .unwrap(); assert_eq!(reconciled.catalog.unwrap().revision, 4); + let targeted = connectors + .reconcile_with_options( + ConnectorReconcileOptions::new("account-1").force_connector_name("github"), + ) + .await + .unwrap(); + assert_eq!(targeted.catalog.unwrap().revision, 4); + timeout(TIMEOUT, server_handle).await.unwrap().unwrap(); } @@ -7261,11 +7833,58 @@ async fn command_execute_handler_error_propagates_to_ack() { // SessionFsProvider tests -------------------------------------------------- use github_copilot_sdk::session_fs::{ - DirEntry, DirEntryKind, FileInfo, FsError, FsErrorKind, SessionFsConventions, - SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, + DirEntry, DirEntryKind, FileInfo, FsError, FsErrorKind, SessionFsCapabilities, SessionFsConfig, + SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, SessionFsSqliteQueryType, SessionFsSqliteTransactionError, SessionFsSqliteTransactionStatement, }; +#[tokio::test] +async fn binary_capability_rejects_text_only_provider_on_create_and_resume() { + for resume in [false, true] { + let (client_write, mut server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let config = SessionFsConfig::new("/workspace", "/sessions", SessionFsConventions::Posix) + .with_capabilities(SessionFsCapabilities::new().with_binary(true)); + let client = Client::from_streams_with_session_fs_config( + client_read, + client_write, + std::env::temp_dir(), + config, + ) + .unwrap(); + let provider = Arc::new(RecordingFsProvider::new()); + let result = timeout(TIMEOUT, async { + if resume { + client + .resume_session( + github_copilot_sdk::ResumeSessionConfig::new(SessionId::new( + "text-only-provider", + )) + .with_session_fs_provider(provider), + ) + .await + } else { + client + .create_session(SessionConfig::default().with_session_fs_provider(provider)) + .await + } + }) + .await + .expect("provider validation must complete before an RPC"); + let error = result.err().expect("text-only provider must be rejected"); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + assert!(error.to_string().contains("binary"), "{error}"); + assert_eq!(client.registered_session_count_for_test(), 0); + assert!( + timeout(Duration::from_millis(50), read_framed(&mut server_read)) + .await + .is_err(), + "invalid provider must not send a session RPC", + ); + drop(server_write); + } +} + struct RecordingFsProvider { files: parking_lot::Mutex>, blocked_stat: Option<(Arc, Arc)>, diff --git a/rust/tests/skill_provider_test.rs b/rust/tests/skill_provider_test.rs new file mode 100644 index 0000000000..71494ee65c --- /dev/null +++ b/rust/tests/skill_provider_test.rs @@ -0,0 +1,965 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![allow(clippy::unwrap_used)] + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::skill_provider::{SkillProvider, SkillProviderDescriptor}; +use github_copilot_sdk::types::{CloudSessionOptions, SessionConfig, SessionId, Tool}; +use github_copilot_sdk::{Client, ClientMode, Error, ErrorKind, ResumeSessionConfig}; +use serde_json::{Value, json}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, duplex}; +use tokio::sync::Notify; +use tokio::time::timeout; + +const TIMEOUT: Duration = Duration::from_secs(2); + +async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), value: &Value) { + let body = serde_json::to_vec(value).unwrap(); + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + writer.write_all(header.as_bytes()).await.unwrap(); + writer.write_all(&body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_framed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + timeout(TIMEOUT, read_framed_untimed(reader)).await.unwrap() +} + +async fn read_framed_untimed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + reader.read_exact(&mut byte).await.unwrap(); + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + + let length: usize = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut body = vec![0; length]; + reader.read_exact(&mut body).await.unwrap(); + serde_json::from_slice(&body).unwrap() +} + +fn make_client() -> (Client, FakeServer) { + make_client_with_mode(ClientMode::default()) +} + +fn make_client_with_mode(mode: ClientMode) -> (Client, FakeServer) { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_mode_for_test( + client_read, + client_write, + std::env::temp_dir(), + mode, + ) + .unwrap(); + ( + client, + FakeServer { + read: server_read, + write: server_write, + session_id: String::new(), + }, + ) +} + +struct FakeServer { + read: DuplexStream, + write: DuplexStream, + session_id: String, +} + +impl FakeServer { + async fn read_request(&mut self) -> Value { + read_framed(&mut self.read).await + } + + async fn respond(&mut self, request: &Value, result: Value) { + let response = json!({ + "jsonrpc": "2.0", + "id": request["id"].as_u64().unwrap(), + "result": result, + }); + write_framed(&mut self.write, &response).await; + } + + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let response = json!({ + "jsonrpc": "2.0", + "id": request["id"].as_u64().unwrap(), + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &response).await; + } + + async fn send_request(&mut self, id: u64, method: &str, params: Value) { + let request = json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + }); + write_framed(&mut self.write, &request).await; + } + + async fn send_notification(&mut self, method: &str, params: Value) { + let notification = json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + }); + write_framed(&mut self.write, ¬ification).await; + } + + async fn read_response(&mut self) -> Value { + read_framed(&mut self.read).await + } +} + +fn descriptor(name: &str, description: &str) -> SkillProviderDescriptor { + SkillProviderDescriptor { + name: name.to_string(), + description: description.to_string(), + ..Default::default() + } +} + +#[derive(Clone, Default)] +struct CallLog(Arc>>); + +impl CallLog { + fn push(&self, call: impl Into) { + self.0.lock().unwrap().push(call.into()); + } + + fn snapshot(&self) -> Vec { + self.0.lock().unwrap().clone() + } +} + +struct RecordingProvider { + skills: Vec, + markdown: HashMap, + calls: CallLog, +} + +impl RecordingProvider { + fn new() -> Self { + Self { + skills: vec![descriptor("review", "Reviews code")], + markdown: HashMap::from([("review".to_string(), "Review carefully.".to_string())]), + calls: CallLog::default(), + } + } + + fn empty() -> Self { + Self { + skills: Vec::new(), + markdown: HashMap::new(), + calls: CallLog::default(), + } + } +} + +#[async_trait] +impl SkillProvider for RecordingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + self.calls.push("list"); + Ok(self.skills.clone()) + } + + async fn read_skill(&self, name: &str) -> std::result::Result, Error> { + self.calls.push(format!("read:{name}")); + Ok(self.markdown.get(name).cloned()) + } +} + +struct FailingProvider { + fail_list: bool, + secret: &'static str, +} + +#[async_trait] +impl SkillProvider for FailingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + if self.fail_list { + Err(Error::with_message(ErrorKind::InvalidConfig, self.secret)) + } else { + Ok(vec![descriptor("review", "Reviews code")]) + } + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + Err(Error::with_message(ErrorKind::InvalidConfig, self.secret)) + } +} + +struct NotifyOnDrop(Arc); + +impl Drop for NotifyOnDrop { + fn drop(&mut self) { + self.0.notify_one(); + } +} + +/// Blocks every call until the SDK drops its future. +struct BlockingProvider { + entered: Arc, + dropped: Arc, +} + +#[async_trait] +impl SkillProvider for BlockingProvider { + async fn list_skills(&self) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } + + async fn read_skill(&self, _name: &str) -> std::result::Result, Error> { + let _dropped = NotifyOnDrop(self.dropped.clone()); + self.entered.notify_one(); + std::future::pending().await + } +} + +fn requested_session_id(request: &Value) -> &str { + request["params"]["sessionId"].as_str().unwrap() +} + +async fn create_session( + client: &Client, + server: &mut FakeServer, + config: SessionConfig, +) -> github_copilot_sdk::session::Session { + let handle = tokio::spawn({ + let client = client.clone(); + async move { client.create_session(config).await.unwrap() } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server.session_id = requested_session_id(&request).to_string(); + server + .respond( + &request, + json!({ + "sessionId": server.session_id, + "workspacePath": "/tmp/workspace", + }), + ) + .await; + timeout(TIMEOUT, handle).await.unwrap().unwrap() +} + +async fn respond_to_resume(server: &mut FakeServer) { + let request = server.read_request().await; + assert_eq!(request["method"], "session.resume"); + server + .respond( + &request, + json!({ + "sessionId": requested_session_id(&request), + "workspacePath": "/tmp/workspace", + }), + ) + .await; + + let reload = server.read_request().await; + assert_eq!(reload["method"], "session.skills.reload"); + server.respond(&reload, json!({})).await; +} + +async fn disconnect_session( + session: &github_copilot_sdk::session::Session, + server: &mut FakeServer, +) { + let disconnect = session.disconnect(); + tokio::pin!(disconnect); + let detach = tokio::select! { + request = server.read_request() => request, + result = &mut disconnect => panic!("disconnect finished before detach request: {result:?}"), + }; + assert_eq!(detach["method"], "session.detach"); + server + .respond(&detach, json!({ "success": true, "error": null })) + .await; + timeout(TIMEOUT, disconnect).await.unwrap().unwrap(); +} + +fn assert_no_provider(response: &Value, session_id: &str) { + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + format!("No skill provider for session: {session_id}") + ); + assert!(response["error"].get("data").is_none()); +} + +fn assert_session_not_found(response: &Value, session_id: &str) { + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + format!("Session not found: {session_id}") + ); +} + +#[test] +fn descriptor_omits_unset_optional_fields_and_config_debug_redacts_provider() { + let value = serde_json::to_value(descriptor("review", "Reviews code")).unwrap(); + assert_eq!( + value, + json!({ "name": "review", "description": "Reviews code" }) + ); + + let provider: Arc = Arc::new(RecordingProvider::new()); + let config = SessionConfig::default().with_skill_provider(provider.clone()); + let cloned = config.clone(); + assert!(Arc::ptr_eq( + config.skill_provider.as_ref().unwrap(), + cloned.skill_provider.as_ref().unwrap() + )); + let debug = format!("{config:?}"); + assert!(debug.contains("skill_provider")); + assert!(debug.contains("")); + + let resume = + ResumeSessionConfig::new(SessionId::new("resume-id")).with_skill_provider(provider.clone()); + let cloned_resume = resume.clone(); + assert!(Arc::ptr_eq( + resume.skill_provider.as_ref().unwrap(), + cloned_resume.skill_provider.as_ref().unwrap() + )); + assert!(format!("{resume:?}").contains("skill_provider")); +} + +#[tokio::test] +async fn create_and_resume_payloads_flag_only_when_provider_is_supplied() { + let (client, mut server) = make_client(); + + let session = create_session(&client, &mut server, SessionConfig::default()).await; + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session.id().clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + .unwrap() + } + }); + respond_to_resume(&mut server).await; + let _resumed = timeout(TIMEOUT, resume).await.unwrap().unwrap(); + + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + let provider = provider.clone(); + async move { + client + .create_session(SessionConfig::default().with_skill_provider(provider)) + .await + .unwrap() + } + }); + let create_request = server.read_request().await; + assert_eq!(create_request["params"]["hasSkillProvider"], true); + assert!(create_request["params"].get("skillProvider").is_none()); + server.session_id = requested_session_id(&create_request).to_string(); + server + .respond( + &create_request, + json!({ "sessionId": server.session_id, "workspacePath": "/tmp/workspace" }), + ) + .await; + let session_with_provider = timeout(TIMEOUT, create).await.unwrap().unwrap(); + + let resume_with_provider = tokio::spawn({ + let client = client.clone(); + let provider = Arc::new(RecordingProvider::new()); + let session_id = session_with_provider.id().clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id).with_skill_provider(provider)) + .await + .unwrap() + } + }); + let resume_request = server.read_request().await; + assert_eq!(resume_request["method"], "session.resume"); + assert_eq!(resume_request["params"]["hasSkillProvider"], true); + assert!(resume_request["params"].get("skillProvider").is_none()); + server + .respond( + &resume_request, + json!({ "sessionId": requested_session_id(&resume_request), "workspacePath": "/tmp/workspace" }), + ) + .await; + let reload = server.read_request().await; + server.respond(&reload, json!({})).await; + let _ = timeout(TIMEOUT, resume_with_provider) + .await + .unwrap() + .unwrap(); +} + +#[tokio::test] +async fn empty_mode_keeps_enable_skills_default_with_provider() { + let (client, mut server) = make_client_with_mode(ClientMode::Empty); + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_available_tools(Vec::::new()) + .with_tools(Vec::::new()) + .with_skill_provider(provider), + ) + .await + .unwrap() + } + }); + let request = server.read_request().await; + assert_eq!(request["params"]["enableSkills"], false); + assert_eq!(request["params"]["hasSkillProvider"], true); + server.session_id = requested_session_id(&request).to_string(); + server + .respond( + &request, + json!({ "sessionId": server.session_id, "workspacePath": "/tmp/workspace" }), + ) + .await; + let update = server.read_request().await; + assert_eq!(update["method"], "session.options.update"); + server.respond(&update, json!({ "success": true })).await; + let _ = timeout(TIMEOUT, create).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn serves_early_create_and_resume_callbacks() { + let (client, mut server) = make_client(); + let provider: Arc = Arc::new(RecordingProvider::new()); + let create = tokio::spawn({ + let client = client.clone(); + let provider = provider.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("early-create") + .with_skill_provider(provider), + ) + .await + .unwrap() + } + }); + let create_request = server.read_request().await; + assert_eq!(create_request["method"], "session.create"); + server + .send_request( + 10, + "skillProvider.list", + json!({ "sessionId": "early-create" }), + ) + .await; + let list_response = server.read_response().await; + assert_eq!(list_response["result"]["skills"][0]["name"], "review"); + server + .respond( + &create_request, + json!({ "sessionId": "early-create", "workspacePath": "/tmp/workspace" }), + ) + .await; + let _ = timeout(TIMEOUT, create).await.unwrap().unwrap(); + + let resume_provider: Arc = Arc::new(RecordingProvider::new()); + let resume = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::new("early-resume")) + .with_skill_provider(resume_provider), + ) + .await + .unwrap() + } + }); + let resume_request = server.read_request().await; + assert_eq!(resume_request["method"], "session.resume"); + server + .send_request( + 11, + "skillProvider.list", + json!({ "sessionId": "early-resume" }), + ) + .await; + let list_response = server.read_response().await; + assert_eq!(list_response["result"]["skills"][0]["name"], "review"); + server + .respond( + &resume_request, + json!({ "sessionId": "early-resume", "workspacePath": "/tmp/workspace" }), + ) + .await; + let reload = server.read_request().await; + server.respond(&reload, json!({})).await; + let _ = timeout(TIMEOUT, resume).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn cloud_sessions_reject_provider_before_rpc() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let error = match client + .create_session( + SessionConfig::default() + .with_skill_provider(provider) + .with_cloud(CloudSessionOptions::default()), + ) + .await + { + Ok(_) => panic!("cloud session unexpectedly accepted a skill provider"), + Err(error) => error, + }; + + assert_eq!( + error.to_string(), + "Skill providers are not supported for cloud sessions." + ); + assert_eq!(calls.snapshot(), Vec::::new()); + assert!( + timeout(Duration::from_millis(100), server.read_request()) + .await + .is_err() + ); +} + +#[tokio::test] +async fn dispatches_list_read_empty_and_not_found() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider { + skills: vec![ + descriptor("review", "Reviews code"), + SkillProviderDescriptor { + name: "deploy".to_string(), + description: "Deploys service".to_string(), + argument_hint: Some("[environment]".to_string()), + disable_model_invocation: Some(true), + user_invocable: Some(false), + }, + ], + markdown: HashMap::from([("deploy".to_string(), "# deploy".to_string())]), + calls: CallLog::default(), + }); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + + server + .send_request(20, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!( + response["result"]["skills"][0], + json!({ + "name": "review", + "description": "Reviews code", + }) + ); + assert_eq!( + response["result"]["skills"][1]["argumentHint"], + "[environment]" + ); + assert_eq!( + response["result"]["skills"][1]["disableModelInvocation"], + true + ); + assert_eq!(response["result"]["skills"][1]["userInvocable"], false); + + server + .send_request( + 21, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "deploy" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "markdown": "# deploy" })); + + server + .send_request( + 22, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "missing" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "markdown": null })); + + let empty = Arc::new(RecordingProvider::empty()); + let empty_session = create_session( + &client, + &mut server, + SessionConfig::default() + .with_session_id("empty-catalog") + .with_skill_provider(empty), + ) + .await; + server + .send_request( + 23, + "skillProvider.list", + json!({ "sessionId": empty_session.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"], json!({ "skills": [] })); +} + +#[tokio::test] +async fn provider_errors_are_generic_and_invalid_params_are_rejected() { + let (client, mut server) = make_client(); + let secret = "db-password-in-error"; + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(FailingProvider { + fail_list: true, + secret, + })), + ) + .await; + let session_id = session.id().to_string(); + + server + .send_request(30, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "Skill provider listSkills failed" + ); + assert!(response["error"].get("data").is_none()); + assert!(!response.to_string().contains(secret)); + + let read_session = create_session( + &client, + &mut server, + SessionConfig::default() + .with_session_id("failing-read") + .with_skill_provider(Arc::new(FailingProvider { + fail_list: false, + secret, + })), + ) + .await; + server + .send_request( + 31, + "skillProvider.read", + json!({ "sessionId": read_session.id(), "name": "review" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "Skill provider readSkill failed" + ); + assert!(response["error"].get("data").is_none()); + assert!(!response.to_string().contains(secret)); + + server + .send_request( + 32, + "skillProvider.read", + json!({ "sessionId": read_session.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["error"]["code"], -32602); +} + +#[tokio::test] +async fn cancel_request_drops_provider_future_and_reports_cancellation() { + let (client, mut server) = make_client(); + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(BlockingProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await; + + for (id, method, params) in [ + ( + 60, + "skillProvider.list", + json!({ "sessionId": session.id() }), + ), + ( + 61, + "skillProvider.read", + json!({ "sessionId": session.id(), "name": "review" }), + ), + ] { + server.send_request(id, method, params).await; + timeout(TIMEOUT, entered.notified()).await.unwrap(); + server + .send_notification("$/cancelRequest", json!({ "id": id })) + .await; + + let response = server.read_response().await; + assert_eq!(response["id"], id); + assert_eq!(response["error"]["code"], -32800); + timeout(TIMEOUT, dropped.notified()).await.unwrap(); + } +} + +#[tokio::test] +async fn connection_close_drops_provider_future() { + let (client, mut server) = make_client(); + let entered = Arc::new(Notify::new()); + let dropped = Arc::new(Notify::new()); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(Arc::new(BlockingProvider { + entered: entered.clone(), + dropped: dropped.clone(), + })), + ) + .await; + + server + .send_request( + 62, + "skillProvider.list", + json!({ "sessionId": session.id() }), + ) + .await; + timeout(TIMEOUT, entered.notified()).await.unwrap(); + drop(server); + + timeout(TIMEOUT, dropped.notified()).await.unwrap(); +} + +#[tokio::test] +async fn missing_provider_reports_generic_error() { + let (client, mut server) = make_client(); + let session = create_session(&client, &mut server, SessionConfig::default()).await; + let session_id = session.id().to_string(); + server + .send_request( + 41, + "skillProvider.read", + json!({ "sessionId": session_id, "name": "review" }), + ) + .await; + let response = server.read_response().await; + assert_no_provider(&response, &session_id); +} + +#[tokio::test] +async fn teardown_and_failed_open_stop_serving_provider() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + disconnect_session(&session, &mut server).await; + + let failed_provider = Arc::new(RecordingProvider::new()); + let failed_calls = failed_provider.calls.clone(); + let failed_create = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("failed-open") + .with_skill_provider(failed_provider), + ) + .await + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server + .respond_error(&request, -32603, "create failed") + .await; + let error = match timeout(TIMEOUT, failed_create).await.unwrap().unwrap() { + Ok(_) => panic!("failed-open session unexpectedly succeeded"), + Err(error) => error, + }; + assert!(error.to_string().contains("create failed")); + server + .send_request(50, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 50); + assert_session_not_found(&response, &session_id); + server + .send_request( + 51, + "skillProvider.list", + json!({ "sessionId": "failed-open" }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 51); + assert_session_not_found(&response, "failed-open"); + assert_eq!(calls.snapshot(), Vec::::new()); + assert_eq!(failed_calls.snapshot(), Vec::::new()); +} + +#[tokio::test] +async fn dropped_session_answers_provider_callbacks_with_an_error() { + let (client, mut server) = make_client(); + let provider = Arc::new(RecordingProvider::new()); + let calls = provider.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(provider), + ) + .await; + let session_id = session.id().to_string(); + drop(session); + + server + .send_request(60, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 60); + assert_session_not_found(&response, &session_id); + + server + .send_request(61, "skillProvider.read", json!({ "name": "review" })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 61); + assert_eq!(response["error"]["code"], -32602); + assert_eq!(calls.snapshot(), Vec::::new()); +} + +#[tokio::test] +async fn resume_rebinds_provider() { + let (client, mut server) = make_client(); + let original = Arc::new(RecordingProvider::new()); + let original_calls = original.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(original), + ) + .await; + let session_id = session.id().clone(); + disconnect_session(&session, &mut server).await; + + let replacement = Arc::new(RecordingProvider::new()); + let replacement_calls = replacement.calls.clone(); + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(session_id).with_skill_provider(replacement), + ) + .await + .unwrap() + } + }); + respond_to_resume(&mut server).await; + let resumed = timeout(TIMEOUT, resume).await.unwrap().unwrap(); + server + .send_request( + 70, + "skillProvider.list", + json!({ "sessionId": resumed.id() }), + ) + .await; + let response = server.read_response().await; + assert_eq!(response["result"]["skills"][0]["name"], "review"); + assert_eq!(original_calls.snapshot(), Vec::::new()); + assert_eq!(replacement_calls.snapshot(), vec!["list"]); +} + +#[tokio::test] +async fn failed_resume_restores_the_resident_provider() { + let (client, mut server) = make_client(); + let resident = Arc::new(RecordingProvider::new()); + let resident_calls = resident.calls.clone(); + let session = create_session( + &client, + &mut server, + SessionConfig::default().with_skill_provider(resident), + ) + .await; + let session_id = session.id().clone(); + + let replacement = Arc::new(RecordingProvider::new()); + let replacement_calls = replacement.calls.clone(); + let resume = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(session_id).with_skill_provider(replacement), + ) + .await + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.resume"); + server + .respond_error(&request, -32603, "resume failed") + .await; + let error = match timeout(TIMEOUT, resume).await.unwrap().unwrap() { + Ok(_) => panic!("resume unexpectedly succeeded"), + Err(error) => error, + }; + assert!(error.to_string().contains("resume failed")); + + // The runtime keeps the resident binding when a resume fails. + server + .send_request(80, "skillProvider.list", json!({ "sessionId": session_id })) + .await; + let response = server.read_response().await; + assert_eq!(response["id"], 80); + assert_eq!(response["result"]["skills"][0]["name"], "review"); + assert_eq!(resident_calls.snapshot(), vec!["list"]); + assert_eq!(replacement_calls.snapshot(), Vec::::new()); + drop(session); +} diff --git a/scripts/build-prerequisites.mjs b/scripts/build-prerequisites.mjs index 63624b33e9..0b0c1d19fc 100644 --- a/scripts/build-prerequisites.mjs +++ b/scripts/build-prerequisites.mjs @@ -6,6 +6,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { installNpmDependencies } from "./install-dependencies.mjs"; export const SCHEMA_FILES = ["api.schema.json", "session-events.schema.json"]; export const GENERATED_ROOTS = { @@ -104,10 +105,10 @@ export function bazelActionEnvironmentArgument() { return "--action_env=PATH"; } -function installCodegenDependencies(languages, sdkRoot) { - runNpm(["ci", "--ignore-scripts"], path.join(sdkRoot, "scripts/codegen")); +export function installCodegenDependencies(languages, sdkRoot) { + installNpmDependencies(path.join(sdkRoot, "scripts/codegen"), runNpm); if (languages.includes("java")) { - runNpm(["ci", "--ignore-scripts"], path.join(sdkRoot, "java/scripts/codegen")); + installNpmDependencies(path.join(sdkRoot, "java/scripts/codegen"), runNpm); } } @@ -155,7 +156,11 @@ export function syncGeneratedArchive({ try { const extraction = archiveExtractionInvocation(archivePath, stagingDirectory); run(tarCommand(), extraction.args, extraction.cwd); - for (const file of listFiles(stagingDirectory)) { + const files = listFiles(stagingDirectory); + if (files.length === 0) { + throw new Error(`No ${language} generated outputs in ${archivePath}`); + } + for (const file of files) { if (!matchesGeneratedPath(file, generatedRoots)) { throw new Error(`Undeclared ${language} generated output: ${file}`); } diff --git a/scripts/build-prerequisites.test.mjs b/scripts/build-prerequisites.test.mjs index 2114c0e207..feae19fedd 100644 --- a/scripts/build-prerequisites.test.mjs +++ b/scripts/build-prerequisites.test.mjs @@ -3,8 +3,10 @@ *--------------------------------------------------------------------------------------------*/ import assert from "node:assert/strict"; +import childProcess from "node:child_process"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; +import { syncBuiltinESMExports } from "node:module"; import os from "node:os"; import path from "node:path"; import test from "node:test"; @@ -13,6 +15,7 @@ import { archiveExtractionInvocation, bazelActionEnvironmentArgument, bazelInvocationEnvironment, + installCodegenDependencies, npmInvocation, parseBazelInfoPath, syncGeneratedArchive, @@ -72,6 +75,48 @@ test("runs npm through the Windows command interpreter", () => { }); }); +test("installs shared codegen dependencies once and Java dependencies only when selected", (t) => { + const sdkRoot = fs.mkdtempSync(path.join(import.meta.dirname, ".codegen-dependencies-")); + t.after(() => fs.rmSync(sdkRoot, { recursive: true, force: true })); + for (const directory of ["scripts/codegen", "java/scripts/codegen"]) { + writeFile(path.join(sdkRoot, directory, "package.json"), "{}"); + writeFile(path.join(sdkRoot, directory, "package-lock.json"), "{}"); + } + const calls = []; + const spawn = t.mock.method(childProcess, "spawnSync", (command, args, { cwd }) => { + calls.push({ command, args, cwd }); + return { status: 0 }; + }); + syncBuiltinESMExports(); + t.after(() => { + spawn.mock.restore(); + syncBuiltinESMExports(); + }); + const invocation = npmInvocation(); + const shared = path.join(sdkRoot, "scripts/codegen"); + const java = path.join(sdkRoot, "java/scripts/codegen"); + + installCodegenDependencies(["nodejs", "rust"], sdkRoot); + installCodegenDependencies(["nodejs"], sdkRoot); + assert.deepEqual(calls, [ + { + command: invocation.command, + args: [...invocation.args, "ci", "--ignore-scripts", "--include=dev"], + cwd: shared, + }, + ]); + assert.equal(fs.existsSync(path.join(java, "node_modules")), false); + + installCodegenDependencies(["java"], sdkRoot); + installCodegenDependencies(["java"], sdkRoot); + assert.equal(calls.length, 2); + assert.deepEqual(calls[1], { + command: invocation.command, + args: [...invocation.args, "ci", "--ignore-scripts", "--include=dev"], + cwd: java, + }); +}); + test("syncs generated projections once without rewriting unchanged files", (t) => { const root = createGitFixture(t); const sdkRoot = path.join(root, "src/sdk"); @@ -134,6 +179,32 @@ test("refuses to overwrite modified generated projections", (t) => { assert.equal(fs.readFileSync(path.join(sdkRoot, generatedPath), "utf8"), "local edit\n"); }); +test("rejects empty generator archives without deleting existing projections", (t) => { + const root = createGitFixture(t); + const sdkRoot = path.join(root, "src/sdk"); + const generatedPath = "nodejs/src/generated/rpc.ts"; + writeFile(path.join(sdkRoot, generatedPath), "tracked\n"); + run("git", ["add", "."], root); + run("git", ["commit", "-m", "fixture"], root); + const stagedRoot = path.join(root, "staged"); + fs.mkdirSync(stagedRoot); + const archivePath = path.join(root, "projection.tar"); + run(tarCommand(), ["-cf", archivePath, "-C", stagedRoot, "."], root); + + assert.throws( + () => + syncGeneratedArchive({ + archivePath, + generatedRoots: ["nodejs/src/generated"], + language: "nodejs", + runtimeRoot: root, + sdkRoot, + }), + /No nodejs generated outputs/, + ); + assert.equal(fs.readFileSync(path.join(sdkRoot, generatedPath), "utf8"), "tracked\n"); +}); + test("replaces prior generated projections while preserving manual edits", (t) => { const root = createGitFixture(t); const sdkRoot = path.join(root, "src/sdk"); diff --git a/scripts/ci/check-generated.mjs b/scripts/ci/check-generated.mjs index 2298ea57be..3eb90b508a 100644 --- a/scripts/ci/check-generated.mjs +++ b/scripts/ci/check-generated.mjs @@ -23,7 +23,7 @@ export const PROTOCOL_FILES = [ const SDK_INPUTS = [ /^scripts\/codegen\//, /^java\/scripts\/codegen\//, - /^scripts\/(?:build-prerequisites|run-tasks|runtime-layout)\.mjs$/, + /^scripts\/(?:build-prerequisites|install-dependencies|run-tasks|runtime-layout)\.mjs$/, /^scripts\/ci\/check-generated\.mjs$/, /^(?:BUILD\.bazel|package\.json|\.editorconfig)$/, /^nodejs\/(?:scripts\/releaseArtifacts\.ts|src\/cliVersion\.ts|package(?:-lock)?\.json)$/, diff --git a/scripts/ci/check-generated.test.mjs b/scripts/ci/check-generated.test.mjs index 38883ec7dd..23c95ced04 100644 --- a/scripts/ci/check-generated.test.mjs +++ b/scripts/ci/check-generated.test.mjs @@ -150,6 +150,7 @@ test("skips projections for internal-only and SDK documentation changes", (t) => for (const file of [ "src/sdk/scripts/codegen/go.ts", "src/sdk/scripts/codegen/package-lock.json", + "src/sdk/scripts/install-dependencies.mjs", "src/sdk/java/scripts/codegen/java.ts", "src/sdk/rust/.rustfmt.nightly.toml", "src/sdk/go/types.go", diff --git a/scripts/ci/codegen-entrypoints.test.mjs b/scripts/ci/codegen-entrypoints.test.mjs new file mode 100644 index 0000000000..b1c522cb2b --- /dev/null +++ b/scripts/ci/codegen-entrypoints.test.mjs @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, symlinkSync } from "node:fs"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const codegenRoot = fileURLToPath(new URL("../codegen/", import.meta.url)); +const loader = createRequire(new URL("../codegen/package.json", import.meta.url)).resolve("tsx"); + +for (const language of ["python", "go", "csharp"]) { + test(`runs the ${language} generator through a Bazel-style symlink`, (t) => { + const directory = mkdtempSync(join(tmpdir(), "codegen-entrypoint-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const entry = join(directory, `${language}.ts`); + symlinkSync(join(codegenRoot, `${language}.ts`), entry); + const missingSchema = join(directory, "missing-schema.json"); + // Supplying both schema paths prevents standalone generators from acquiring a published runtime. + const result = spawnSync( + process.execPath, + ["--import", pathToFileURL(loader).href, entry, missingSchema, missingSchema], + { + cwd: directory, + encoding: "utf8", + windowsHide: true, + timeout: process.platform === "win32" ? 60_000 : 30_000, + env: { ...process.env, COPILOT_CODEGEN_OUTPUT_ROOT: join(directory, "output") }, + }, + ); + const diagnostics = `${result.stdout}\n${result.stderr}`; + assert.ifError(result.error); + assert.notEqual(result.status, 0, `Expected ${language} to reject the missing local schemas.\n${diagnostics}`); + assert.match(result.stderr, /missing-schema\.json/, diagnostics); + }); +} diff --git a/scripts/ci/install-dependencies.test.mjs b/scripts/ci/install-dependencies.test.mjs new file mode 100644 index 0000000000..0799228417 --- /dev/null +++ b/scripts/ci/install-dependencies.test.mjs @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +import { installNpmDependencies } from "../install-dependencies.mjs"; +import { npmInvocation } from "../build-prerequisites.mjs"; + +test("installs once and fingerprints the install arguments and manifests", (t) => { + const { directory, stampFile } = fixture(t); + const calls = []; + const runNpm = (args, cwd) => calls.push({ args, cwd }); + + assert.equal(installNpmDependencies(directory, runNpm), true); + assert.deepEqual(calls, [{ args: ["ci", "--ignore-scripts", "--include=dev"], cwd: directory }]); + const expectedFingerprint = createHash("sha256") + .update(JSON.stringify(["ci", "--ignore-scripts", "--include=dev"])) + .update(fs.readFileSync(path.join(directory, "package-lock.json"))) + .update(fs.readFileSync(path.join(directory, "package.json"))) + .digest("hex"); + assert.equal(fs.readFileSync(stampFile, "utf8"), `${expectedFingerprint}\n`); + + assert.equal(installNpmDependencies(directory, runNpm), false); + assert.equal(calls.length, 1); +}); + +for (const file of ["package.json", "package-lock.json"]) { + test(`reinstalls when ${file} changes`, (t) => { + const { directory, stampFile } = fixture(t); + const calls = []; + const runNpm = (args) => calls.push(args); + installNpmDependencies(directory, runNpm); + const previousStamp = fs.readFileSync(stampFile, "utf8"); + fs.appendFileSync(path.join(directory, file), "\n"); + + assert.equal(installNpmDependencies(directory, runNpm), true); + assert.equal(calls.length, 2); + assert.notEqual(fs.readFileSync(stampFile, "utf8"), previousStamp); + }); +} + +for (const missing of ["stamp", "node_modules"]) { + test(`reinstalls when ${missing} is missing`, (t) => { + const { directory, stampFile } = fixture(t); + installNpmDependencies(directory, () => {}); + fs.rmSync(missing === "stamp" ? stampFile : path.dirname(stampFile), { recursive: true }); + + const calls = []; + assert.equal( + installNpmDependencies(directory, (args) => calls.push(args)), + true, + ); + assert.deepEqual(calls, [["ci", "--ignore-scripts", "--include=dev"]]); + assert.equal(fs.existsSync(stampFile), true); + }); +} + +test("removes the old stamp before installing and leaves it absent after failure", (t) => { + const { directory, stampFile } = fixture(t); + installNpmDependencies(directory, () => {}); + fs.appendFileSync(path.join(directory, "package-lock.json"), "\n"); + const failure = new Error("npm ci failed"); + let attempts = 0; + + assert.throws( + () => + installNpmDependencies(directory, () => { + attempts++; + assert.equal(fs.existsSync(stampFile), false); + throw failure; + }), + (error) => error === failure, + ); + assert.equal(attempts, 1); + assert.equal(fs.existsSync(stampFile), false); + assert.equal( + installNpmDependencies(directory, () => {}), + true, + ); + assert.equal(fs.existsSync(stampFile), true); +}); + +test("surfaces unexpected stamp read errors without attempting an install", (t) => { + const { directory, stampFile } = fixture(t); + fs.mkdirSync(stampFile, { recursive: true }); + let installed = false; + + assert.throws( + () => + installNpmDependencies(directory, () => { + installed = true; + }), + { code: "EISDIR" }, + ); + assert.equal(installed, false); +}); + +for (const environment of [{ NODE_ENV: "production" }, { npm_config_omit: "dev" }]) { + test(`installs build tools despite ${JSON.stringify(environment)} and reuses them on a normal retry`, (t) => { + const { directory, runNpm, buildTool } = npmFixture(t); + + assert.equal( + installNpmDependencies(directory, (args, cwd) => runNpm(args, cwd, environment)), + true, + ); + assert.equal(fs.existsSync(buildTool), true); + assert.equal(installNpmDependencies(directory, runNpm), false); + assert.equal(fs.existsSync(buildTool), true); + }); +} + +test("repairs a production-only install recorded by the old manifest-only stamp", (t) => { + const { directory, stampFile, runNpm, buildTool } = npmFixture(t); + runNpm(["ci", "--ignore-scripts"], directory, { NODE_ENV: "production" }); + assert.equal(fs.existsSync(buildTool), false); + const legacyFingerprint = createHash("sha256") + .update(fs.readFileSync(path.join(directory, "package-lock.json"))) + .update(fs.readFileSync(path.join(directory, "package.json"))) + .digest("hex"); + fs.mkdirSync(path.dirname(stampFile), { recursive: true }); + fs.writeFileSync(stampFile, `${legacyFingerprint}\n`); + + assert.equal(installNpmDependencies(directory, runNpm), true); + assert.equal(fs.existsSync(buildTool), true); + assert.equal(installNpmDependencies(directory, runNpm), false); +}); + +function npmFixture(t) { + const fixturePaths = fixture(t); + const { directory } = fixturePaths; + fs.mkdirSync(path.join(directory, "build-tool")); + fs.writeFileSync( + path.join(directory, "build-tool/package.json"), + JSON.stringify({ name: "fixture-build-tool", version: "1.0.0" }), + ); + fs.writeFileSync( + path.join(directory, "package.json"), + JSON.stringify({ name: "fixture", devDependencies: { "fixture-build-tool": "file:./build-tool" } }), + ); + const environment = Object.fromEntries( + Object.entries(process.env).filter(([name]) => name !== "NODE_ENV" && !/^npm_config_/i.test(name)), + ); + const userConfig = path.join(directory, "user.npmrc"); + const globalConfig = path.join(directory, "global.npmrc"); + fs.writeFileSync(userConfig, ""); + fs.writeFileSync(globalConfig, ""); + const invocation = npmInvocation(); + const runNpm = (args, cwd, overrides = {}) => { + const result = spawnSync( + invocation.command, + [ + ...invocation.args, + ...args, + "--offline", + "--no-audit", + "--no-fund", + `--userconfig=${userConfig}`, + `--globalconfig=${globalConfig}`, + `--cache=${path.join(directory, "cache")}`, + ], + { cwd, env: { ...environment, ...overrides }, encoding: "utf8", timeout: 30_000 }, + ); + assert.equal(result.status, 0, result.error?.message ?? `${result.stdout}\n${result.stderr}`); + }; + runNpm(["install", "--package-lock-only", "--ignore-scripts"], directory); + return { + ...fixturePaths, + runNpm, + buildTool: path.join(directory, "node_modules/fixture-build-tool/package.json"), + }; +} + +function fixture(t) { + const directory = fs.mkdtempSync(path.join(import.meta.dirname, ".install-dependencies-")); + t.after(() => fs.rmSync(directory, { recursive: true, force: true })); + fs.writeFileSync(path.join(directory, "package.json"), '{"name":"fixture"}\n'); + fs.writeFileSync(path.join(directory, "package-lock.json"), '{"lockfileVersion":3}\n'); + return { directory, stampFile: path.join(directory, "node_modules/.copilot-sdk-install-stamp") }; +} diff --git a/scripts/ci/run-dotnet-tests.sh b/scripts/ci/run-dotnet-tests.sh index a178357477..ea0312c4a6 100755 --- a/scripts/ci/run-dotnet-tests.sh +++ b/scripts/ci/run-dotnet-tests.sh @@ -11,6 +11,7 @@ Usage: run-dotnet-tests.sh [--help] Runs the full .NET SDK test project. Environment variables: DOTNET_TEST_FILTER Optional dotnet test filter (e.g. for a backend or transport). DOTNET_TEST_RUNTIME Optional runtime identifier passed to dotnet test. + DOTNET_TEST_RESULTS_DIRECTORY Results directory (default: TestResults). EOF } @@ -34,7 +35,7 @@ args=( --blame-hang-timeout 10m --blame-hang-dump-type none --logger "trx;LogFilePrefix=test-results" - --results-directory TestResults + --results-directory "${DOTNET_TEST_RESULTS_DIRECTORY:-TestResults}" -p:RunAnalyzers=false ) filter="${DOTNET_TEST_FILTER:-}" diff --git a/scripts/ci/run-dotnet-tests.test.mjs b/scripts/ci/run-dotnet-tests.test.mjs index 6a1be129e1..582fcd6b2c 100644 --- a/scripts/ci/run-dotnet-tests.test.mjs +++ b/scripts/ci/run-dotnet-tests.test.mjs @@ -19,7 +19,7 @@ function fromBashPath(value) { return value; } -await test("runs all .NET tests and forwards only explicit filter and runtime", (t) => { +await test("runs all .NET tests with optional filter, runtime, and results directory", (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "sdk-dotnet-test-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const argumentsPath = path.join(root, "arguments"); @@ -31,9 +31,9 @@ await test("runs all .NET tests and forwards only explicit filter and runtime", ); fs.chmodSync(fakeDotnet, 0o755); - for (const [filter, runtime] of [ - ["", ""], - ["E2EBackend!=CapiOnly", "win-x64"], + for (const [filter, runtime, resultsDirectory] of [ + ["", "", ""], + ["E2EBackend!=CapiOnly", "win-x64", "TestResults/inprocess"], ]) { const result = spawnSync("bash", [script], { encoding: "utf8", @@ -42,6 +42,7 @@ await test("runs all .NET tests and forwards only explicit filter and runtime", ARGUMENTS_PATH: argumentsPath, DOTNET_TEST_FILTER: filter, DOTNET_TEST_RUNTIME: runtime, + DOTNET_TEST_RESULTS_DIRECTORY: resultsDirectory, PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`, WORKING_DIRECTORY_PATH: workingDirectoryPath, }, @@ -54,6 +55,10 @@ await test("runs all .NET tests and forwards only explicit filter and runtime", ); const arguments_ = fs.readFileSync(argumentsPath, "utf8").trim().split("\n"); assert.deepEqual(arguments_.slice(0, 3), ["test", "test/GitHub.Copilot.SDK.Test.csproj", "--no-build"]); + assert.equal( + arguments_[arguments_.indexOf("--results-directory") + 1], + resultsDirectory || "TestResults", + ); assert.equal(arguments_.includes("--filter"), filter !== ""); if (filter) { assert.equal(arguments_[arguments_.indexOf("--filter") + 1], filter); @@ -70,6 +75,7 @@ await test("documents its environment and rejects positional arguments", () => { assert.equal(help.status, 0, help.stderr); assert.match(help.stdout, /DOTNET_TEST_FILTER/); assert.match(help.stdout, /DOTNET_TEST_RUNTIME/); + assert.match(help.stdout, /DOTNET_TEST_RESULTS_DIRECTORY/); const invalid = spawnSync("bash", [script, "unexpected"], { encoding: "utf8" }); assert.equal(invalid.status, 2); diff --git a/scripts/ci/run-tasks.test.mjs b/scripts/ci/run-tasks.test.mjs index e4c5bfd7c9..7c205a2255 100644 --- a/scripts/ci/run-tasks.test.mjs +++ b/scripts/ci/run-tasks.test.mjs @@ -3,7 +3,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from "node:assert/strict"; +import childProcess from "node:child_process"; +import fs from "node:fs"; +import { syncBuiltinESMExports } from "node:module"; +import path from "node:path"; import test from "node:test"; +import { pathToFileURL } from "node:url"; import { getTaskSteps } from "../run-tasks.mjs"; @@ -33,3 +38,117 @@ test("retains the explicit SDK Rust formatting command", () => { }, ]); }); + +test("default build selects the existing Node and Rust build steps in order", () => { + assert.deepEqual(getTaskSteps("build:default"), [ + ...getTaskSteps("build", "nodejs"), + ...getTaskSteps("build", "rust"), + ]); + assert.throws(() => getTaskSteps("build:default", "java"), /Unknown SDK task/); +}); + +test("checkout default build prepares both projections once without rebuilding the CLI", async (t) => { + const { runTasks, preparations, calls, sdkRoot, root } = await taskFixture(t, true); + + runTasks("build:default", undefined, { environment: {}, runtimeSource: "checkout" }); + + assert.deepEqual(preparations, [{ languages: ["nodejs", "rust"], runtimeRoot: root, sdkRoot }]); + assert.deepEqual(calls, [ + { executable: "npm", args: ["ci", "--ignore-scripts", "--include=dev"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "npm", args: ["run", "build"], cwd: path.join(sdkRoot, "nodejs") }, + { + executable: process.platform === "win32" ? "pnpm.cmd" : "pnpm", + args: ["bazel", "build", "--action_env=PATH", "//src/sdk/rust:github-copilot-sdk"], + cwd: root, + }, + ]); + + calls.length = 0; + runTasks("build:default", undefined, { environment: {}, runtimeSource: "checkout" }); + assert.equal(preparations.length, 2); + assert.equal(calls.length, 2); + assert.deepEqual(calls[0].args, ["run", "build"]); +}); + +test("standalone default build retains the all-features Cargo build", async (t) => { + const { runTasks, preparations, calls, sdkRoot } = await taskFixture(t, false); + + runTasks("build:default", undefined, { environment: { COPILOT_RUNTIME_SOURCE: "published" } }); + + assert.deepEqual(preparations, []); + assert.deepEqual(calls, [ + { executable: "npm", args: ["ci", "--ignore-scripts", "--include=dev"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "npm", args: ["run", "build"], cwd: path.join(sdkRoot, "nodejs") }, + { executable: "cargo", args: ["build", "--all-features"], cwd: path.join(sdkRoot, "rust") }, + ]); +}); + +for (const language of ["nodejs", "rust"]) { + test(`checkout ${language} build still prepares only its selected projection`, async (t) => { + const { runTasks, preparations, calls, sdkRoot, root } = await taskFixture(t, true); + + runTasks("build", language, { environment: {}, runtimeSource: "checkout" }); + + assert.deepEqual(preparations, [{ languages: [language], runtimeRoot: root, sdkRoot }]); + assert.equal(calls.length, language === "nodejs" ? 2 : 1); + assert.ok(calls.every(({ cwd }) => cwd === (language === "nodejs" ? path.join(sdkRoot, language) : root))); + }); +} + +for (const checkout of [true, false]) { + test(`Java docs install once through the Maven wrapper before validation (${checkout ? "checkout" : "standalone"})`, async (t) => { + const { runTasks, preparations, calls, sdkRoot } = await taskFixture(t, checkout); + + runTasks("docs", "java", { environment: {}, runtimeSource: checkout ? "checkout" : "published" }); + + assert.deepEqual(preparations, []); + assert.deepEqual(calls, [ + { executable: "npm", args: ["run", "extract"], cwd: path.join(sdkRoot, "scripts/docs-validation") }, + { + executable: process.platform === "win32" ? "mvnw.cmd" : "./mvnw", + args: ["install", "-Dmaven.test.skip=true", "-Dskip.test.harness=true", "-Dcopilot.native.skip.download=true"], + cwd: path.join(sdkRoot, "java"), + }, + { executable: "npm", args: ["run", "validate:java"], cwd: path.join(sdkRoot, "scripts/docs-validation") }, + ]); + }); +} + +async function taskFixture(t, checkout) { + const root = fs.mkdtempSync(path.join(import.meta.dirname, ".run-tasks-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const sdkRoot = path.join(root, checkout ? "src/sdk" : "sdk"); + const scripts = path.join(sdkRoot, "scripts"); + fs.mkdirSync(scripts, { recursive: true }); + for (const name of ["run-tasks.mjs", "runtime-layout.mjs", "install-dependencies.mjs"]) { + fs.copyFileSync(new URL(`../${name}`, import.meta.url), path.join(scripts, name)); + } + // Observe the prerequisite boundary without invoking Bazel or changing real generated files. + fs.writeFileSync( + path.join(scripts, "build-prerequisites.mjs"), + `export const preparations = []; +export function prepareSdkSources(options) { preparations.push(options); } +export function bazelActionEnvironmentArgument() { return "--action_env=PATH"; } +`, + ); + fs.mkdirSync(path.join(sdkRoot, "nodejs")); + fs.writeFileSync(path.join(sdkRoot, "nodejs/package.json"), "{}"); + fs.writeFileSync(path.join(sdkRoot, "nodejs/package-lock.json"), "{}"); + if (checkout) { + fs.mkdirSync(path.join(root, "script")); + fs.writeFileSync(path.join(root, "script/sea-build.ts"), ""); + } + const calls = []; + const spawn = t.mock.method(childProcess, "spawnSync", (executable, args, { cwd }) => { + calls.push({ executable, args, cwd }); + return { status: 0 }; + }); + syncBuiltinESMExports(); + t.after(() => { + spawn.mock.restore(); + syncBuiltinESMExports(); + }); + const { runTasks } = await import(pathToFileURL(path.join(scripts, "run-tasks.mjs"))); + const { preparations } = await import(pathToFileURL(path.join(scripts, "build-prerequisites.mjs"))); + return { runTasks, preparations, calls, sdkRoot, root }; +} diff --git a/scripts/ci/runtime-artifact.mjs b/scripts/ci/runtime-artifact.mjs index a6f41275bf..8205c196de 100644 --- a/scripts/ci/runtime-artifact.mjs +++ b/scripts/ci/runtime-artifact.mjs @@ -99,6 +99,8 @@ export function prepareRuntimeArtifact(options = {}) { `package/${executableName}`, "--exclude", "package/app.js", + "--exclude", + "package/cli-main.js", "-czf", path.basename(inProcessArchive), "package", diff --git a/scripts/ci/runtime-artifact.test.mjs b/scripts/ci/runtime-artifact.test.mjs index 329c8dc429..1823844ad3 100644 --- a/scripts/ci/runtime-artifact.test.mjs +++ b/scripts/ci/runtime-artifact.test.mjs @@ -72,6 +72,7 @@ await test("stages all same-checkout runtime inputs", (t) => { const wrapperName = process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; writeFixture(path.join(runtimeRoot, "dist-cli", "index.js"), "entry point"); writeFixture(path.join(runtimeRoot, "dist-cli", "app.js"), "legacy entry point"); + writeFixture(path.join(runtimeRoot, "dist-cli", "cli-main.js"), "CLI UI"); writeFixture(path.join(runtimeRoot, "dist-cli", "runtime-asset"), "asset"); writeFixture(path.join(runtimeRoot, "dist-cli", "copilot-sdk", "index.js"), "sdk entry point"); writeFixture(path.join(runtimeRoot, "dist-cli", "copilot-sdk", "extension.js"), "sdk extension entry point"); @@ -86,6 +87,7 @@ await test("stages all same-checkout runtime inputs", (t) => { assert.equal(fs.readFileSync(values.COPILOT_RUNTIME_LIBRARY_PATH, "utf8"), "runtime"); assert.equal(fs.readFileSync(values.COPILOT_CLI_PATH, "utf8"), "wrapper"); assert.equal(fs.readFileSync(values.COPILOT_LEGACY_CLI_PATH, "utf8"), "legacy entry point"); + assert.equal(fs.readFileSync(path.join(outputDirectory, "package", "cli-main.js"), "utf8"), "CLI UI"); assert.equal( fs.readFileSync(path.join(values.COPILOT_EXTENSION_SDK_PATH, "extension.js"), "utf8"), "sdk extension entry point", @@ -111,6 +113,7 @@ await test("stages all same-checkout runtime inputs", (t) => { assert.match(inProcessContents.stdout, new RegExp(`package/prebuilds/${target}/runtime\\.node`)); assert.doesNotMatch(inProcessContents.stdout, new RegExp(`^package/${executableName}$`, "m")); assert.doesNotMatch(inProcessContents.stdout, /^package\/app\.js$/m); + assert.doesNotMatch(inProcessContents.stdout, /^package\/cli-main\.js$/m); }); await test("preserves complete checked-in Rust release pins", (t) => { diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 63afde9d3d..d274af26d9 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -18,6 +18,7 @@ import { getApiSchemaPath, getRpcSchemaTypeName, getSessionEventsSchemaPath, + isCodegenEntrypoint, writeGeneratedFile, collectExternalSchemaRefNames, collectDefinitionCollections, @@ -586,6 +587,8 @@ const EXPERIMENTAL_ATTRIBUTE = "[Experimental(global::GitHub.Copilot.Diagnostics const EDITOR_BROWSABLE_NEVER_ATTRIBUTE = "[EditorBrowsable(EditorBrowsableState.Never)]"; const OBSOLETE_ATTRIBUTE = `#if NET5_0_OR_GREATER [Obsolete("This member is deprecated and will be removed in a future version.", DiagnosticId = "GHCP001")] +#else +[Obsolete("This member is deprecated and will be removed in a future version.")] #endif`; const STRING_ENUM_RESERVED_MEMBER_NAMES = new Set(["Value", "Equals", "GetHashCode", "ToString", "Converter"]); @@ -636,6 +639,7 @@ interface EventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventDeprecated: boolean; eventExperimental: boolean; dataExperimental: boolean; } @@ -737,6 +741,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataClassName: `${baseName}Data`, dataSchema, dataDescription: dataSchema?.description, + eventDeprecated: isSchemaDeprecated(variant), eventExperimental: isSchemaExperimental(variant), dataExperimental: isSchemaExperimental(dataSchema), }; @@ -1506,6 +1511,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace GitHub.Copilot; `); @@ -1526,12 +1532,96 @@ namespace GitHub.Copilot; lines.push(` /// `, ` /// The event type discriminator.`, ` /// `); lines.push(` [JsonIgnore]`, ` public virtual string Type => "unknown";`, ""); lines.push(` /// Deserializes a JSON string into a .`); - lines.push(` public static SessionEvent FromJson(string json) =>`, ` JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!;`, ""); + lines.push(` public static SessionEvent FromJson(string json) =>`, ` SessionEventJsonConverter.Deserialize(json);`, ""); lines.push(` /// Serializes this event to a JSON string.`); lines.push(` public string ToJson() =>`, ` JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent);`, ""); lines.push(` [DebuggerBrowsable(DebuggerBrowsableState.Never)]`, ` private string DebuggerDisplay => ToJson();`); lines.push(`}`, ""); + lines.push(`internal sealed partial class SessionEventJsonConverter : JsonConverter`, `{`); + lines.push(` internal static SessionEventJsonConverter Default { get; } = new();`, ""); + lines.push(` public override SessionEvent? Read(`, ` ref Utf8JsonReader reader,`, ` Type typeToConvert,`, ` JsonSerializerOptions options)`, ` {`); + lines.push(` // Preserve the reader's position for deserialization.`); + lines.push(` var probe = reader;`); + lines.push(` JsonTypeInfo typeInfo = ReadEventTypeInfo(ref probe);`); + lines.push(` return ToSessionEvent(JsonSerializer.Deserialize(ref reader, typeInfo));`, ` }`, ""); + lines.push(` private static JsonTypeInfo? GetEventTypeInfo(ReadOnlySpan type)`, ` {`); + lines.push(` switch (type.Length)`, ` {`); + const variantsByByteLength = new Map(); + for (const variant of [...variants].sort((a, b) => a.typeName.localeCompare(b.typeName))) { + const byteLength = Buffer.byteLength(variant.typeName, "utf8"); + const group = variantsByByteLength.get(byteLength) ?? []; + group.push(variant); + variantsByByteLength.set(byteLength, group); + } + for (const [byteLength, group] of [...variantsByByteLength].sort(([a], [b]) => a - b)) { + lines.push(` case ${byteLength}:`); + for (const variant of group) { + lines.push(` if (type.SequenceEqual("${escapeCSharpStringLiteral(variant.typeName)}"u8))`); + lines.push(` return SessionEventsJsonContext.Default.${variant.className};`); + } + lines.push(` break;`); + } + lines.push(` }`, ` return null;`, ` }`, ""); + lines.push(` public override void Write(`, ` Utf8JsonWriter writer,`, ` SessionEvent value,`, ` JsonSerializerOptions options) =>`, ` JsonSerializer.Serialize(writer, value, SessionEventsJsonContext.Default.SessionEvent);`, ""); + lines.push(` private static SessionEvent ToSessionEvent(object? value) =>`); + lines.push(` value as SessionEvent ?? ((SessionEventEnvelope?)value)?.ToSessionEvent()!;`, ""); + lines.push(` private static JsonTypeInfo ReadEventTypeInfo(ref Utf8JsonReader reader)`, ` {`); + lines.push(` if (reader.TokenType == JsonTokenType.Null)`, ` {`, ` return SessionEventsJsonContext.Default.SessionEventEnvelope;`, ` }`, ""); + lines.push(` if (reader.TokenType != JsonTokenType.StartObject)`, ` {`); + lines.push(` throw new JsonException("Expected a session event object.");`, ` }`, ""); + lines.push(` JsonTypeInfo? typeInfo = null;`); + lines.push(` bool foundDiscriminator = false;`); + lines.push(` while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName)`, ` {`); + lines.push(` bool isDiscriminator = reader.ValueIsEscaped`); + lines.push(` ? ReadString(ref reader) == "type"`); + lines.push(` : reader.ValueTextEquals("type"u8);`); + lines.push(` if (isDiscriminator)`, ` {`); + lines.push(` if (foundDiscriminator)`, ` {`); + lines.push(` throw new JsonException("Duplicate session event type discriminator.");`, ` }`); + lines.push(` foundDiscriminator = true;`); + lines.push(` reader.Read();`); + lines.push(` if (reader.TokenType == JsonTokenType.String)`, ` {`); + lines.push(` if (reader.HasValueSequence || reader.ValueIsEscaped)`, ` {`); + lines.push(` typeInfo = GetEventTypeInfo(StrictUtf8.GetBytes(ReadString(ref reader)));`, ` }`); + lines.push(` else`, ` {`); + lines.push(` typeInfo = GetEventTypeInfo(reader.ValueSpan);`); + lines.push(` if (typeInfo is null)`, ` {`); + lines.push(` // Validate unknown discriminator text too, including invalid UTF-8.`); + lines.push(` _ = ReadString(ref reader);`, ` }`, ` }`, ` }`); + lines.push(` else if (reader.TokenType != JsonTokenType.Number || !reader.TryGetInt32(out _))`, ` {`); + lines.push(` throw new JsonException("Expected a string or integer session event type discriminator.");`, ` }`, ` }`); + lines.push(` else`, ` {`); + lines.push(` bool isMetadata = reader.HasValueSequence || reader.ValueIsEscaped`); + lines.push(` ? ReadString(ref reader) is { Length: > 0 } propertyName && propertyName[0] == '$'`); + lines.push(` : !reader.ValueSpan.IsEmpty && reader.ValueSpan[0] == (byte)'$';`); + lines.push(` if (isMetadata)`, ` {`); + lines.push(` throw new JsonException("Unexpected session event metadata property.");`, ` }`); + lines.push(` reader.Read();`); + lines.push(` reader.Skip();`, ` }`, ` }`, ""); + lines.push(` return typeInfo ?? SessionEventsJsonContext.Default.SessionEventEnvelope;`, ` }`); + lines.push(`}`, ""); + + lines.push(`internal sealed class SessionEventJsonTypeInfoResolver : IJsonTypeInfoResolver`, `{`); + lines.push(` internal static SessionEventJsonTypeInfoResolver Default { get; } = new();`, ""); + lines.push(` public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)`, ` {`); + lines.push(` if (type != typeof(SessionEvent))`, ` {`, ` return null;`, ` }`, ""); + lines.push(` JsonTypeInfo typeInfo = JsonMetadataServices.CreateValueInfo(`, ` options,`, ` SessionEventJsonConverter.Default);`); + lines.push(` typeInfo.PolymorphismOptions = null;`); + lines.push(` return typeInfo;`, ` }`); + lines.push(`}`, ""); + + lines.push(`internal sealed class SessionEventEnvelope`, `{`); + for (const property of envelopeProperties) { + lines.push(...emitSessionEventEnvelopeProperty(property, knownTypes, nestedClasses, enumOutput)); + } + lines.push(` internal SessionEvent ToSessionEvent() => new()`, ` {`); + for (const property of envelopeProperties) { + const csharpName = toCSharpPropertyName(property.name, property.schema); + lines.push(` ${csharpName} = ${csharpName},`); + } + lines.push(` };`, `}`, ""); + // Event classes with XML docs for (const variant of variants) { const remarksLine = `/// Represents the ${escapeXml(variant.typeName)} event.`; @@ -1544,6 +1634,9 @@ namespace GitHub.Copilot; if (variant.eventExperimental) { pushExperimentalAttribute(lines); } + if (variant.eventDeprecated) { + pushObsoleteAttributes(lines); + } lines.push(`public sealed partial class ${variant.className} : SessionEvent`, `{`); lines.push(` /// `); lines.push(` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); @@ -1573,7 +1666,7 @@ namespace GitHub.Copilot; for (const code of enumOutput) lines.push(code); // JsonSerializerContext - const types = ["SessionEvent", ...variants.flatMap((v) => [v.className, v.dataClassName]), ...nestedClasses.keys()].sort(); + const types = ["SessionEvent", "SessionEventEnvelope", ...variants.flatMap((v) => [v.className, v.dataClassName]), ...nestedClasses.keys()].sort(); lines.push(`[JsonSourceGenerationOptions(`, ` JsonSerializerDefaults.Web,`, ` AllowOutOfOrderMetadataProperties = true,`, ` NumberHandling = JsonNumberHandling.AllowReadingFromString,`, ` DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]`); for (const t of types) lines.push(`[JsonSerializable(typeof(${t}))]`); lines.push(`[JsonSerializable(typeof(JsonElement))]`); @@ -1599,6 +1692,10 @@ export async function generateSessionEvents(schemaPath?: string): Promise // ══════════════════════════════════════════════════════════════════════════════ let emittedRpcClassSchemas = new Map(); +// Class names reachable from a public method's params or result. A type shared +// with an internal method must stay public regardless of which method emits it +// first; see the upgrade in `emitRpcClass`. +const publicReachableRpcClasses = new Set(); const nonSessionRequestTypeNames = new Set(); let emittedRpcEnumResultTypes = new Set(); let experimentalRpcTypes = new Set(); @@ -1942,6 +2039,13 @@ function emitRpcClass( (effectiveSchema as Record).visibility === "internal" ) { visibility = "internal"; + } else if (publicReachableRpcClasses.has(className)) { + // The caller's default follows the *method* being emitted, and only the + // first emission of a class name wins. When an internal method shares a + // type with a public one and is emitted first, that would freeze the + // type as internal and leave the public method referencing it + // (CS0050/CS0051). Reachability from a public method decides instead. + visibility = "public"; } const schemaKey = stableStringify(effectiveSchema); const existingSchema = emittedRpcClassSchemas.get(className); @@ -2813,6 +2917,7 @@ export function generateRpcCode( schema = cloneSchemaForCodegen(schema); omitUnrepresentableInternalProperties(schema); emittedRpcClassSchemas.clear(); + publicReachableRpcClasses.clear(); nonSessionRequestTypeNames.clear(); emittedRpcEnumResultTypes.clear(); experimentalRpcTypes.clear(); @@ -2854,6 +2959,18 @@ export function generateRpcCode( } } } + for (const name of collectRpcMethodReferencedDefinitionNames( + allMethods.filter((method) => method.visibility !== "internal"), + rpcDefinitions + )) { + publicReachableRpcClasses.add(typeToClassName(name)); + } + // A method's own request wrapper is deliberately emitted `internal` at its + // call site; public methods take the individual parameters instead. Only + // types shared *inside* a signature need the upgrade, so drop the wrappers. + for (const method of allMethods) { + publicReachableRpcClasses.delete(paramsTypeName(method)); + } const classes: string[] = []; let serverRpcParts: string[] = []; @@ -3015,7 +3132,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro const __filename = fileURLToPath(import.meta.url); -if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { +if (isCodegenEntrypoint(process.argv[1], __filename)) { const sessionArg = process.argv[2] || undefined; const apiArg = process.argv[3] || undefined; generate(sessionArg, apiArg).catch((err) => { diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 4f148a129f..5e01ee7bf2 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -29,6 +29,7 @@ import { getNullableInner, getRpcSchemaTypeName, getSessionEventsSchemaPath, + isCodegenEntrypoint, getSessionEventVariantSchemas, getSharedSessionEventEnvelopeProperties, hasSchemaPayload, @@ -504,6 +505,7 @@ interface GoEventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventDeprecated: boolean; eventExperimental: boolean; dataExperimental: boolean; } @@ -597,6 +599,7 @@ function extractGoEventVariants(schema: JSONSchema7): GoEventVariant[] { dataClassName: `${toPascalCase(typeName)}Data`, dataSchema, dataDescription: dataSchema.description, + eventDeprecated: isSchemaDeprecated(variant), eventExperimental: isSchemaExperimental(variant), dataExperimental: isSchemaExperimental(dataSchema), }; @@ -3013,8 +3016,54 @@ function emitGoAlias(typeName: string, schema: JSONSchema7, ctx: GoCodegenCtx): ctx.structs.push(lines.join("\n")); } +/** + * A named definition is wire-nullable when its `anyOf` pairs a single `$ref` + * with a real `type: "null"` branch. The `{ "not": {} }` omission sentinel is + * deliberately excluded: it means "absent", not "null on the wire". Returns + * the referenced definition name, or undefined when the shape does not match. + */ +function goWireNullableRefName(schema: JSONSchema7): string | undefined { + if (!Array.isArray(schema.anyOf)) return undefined; + const branches = schema.anyOf.filter((branch): branch is JSONSchema7 => typeof branch === "object" && branch !== null); + if (branches.length !== schema.anyOf.length) return undefined; + if (!branches.some((branch) => branch.type === "null")) return undefined; + const refBranches = branches.filter((branch) => typeof branch.$ref === "string"); + if (refBranches.length !== 1 || branches.length !== 2) return undefined; + return refBranches[0].$ref!.split("/").pop(); +} + +/** + * Emits a nullable result definition as a pointer alias so the exported name + * matches what the corresponding method already returns and a JSON `null` + * cannot decode into a zero value. + */ +function emitGoNullableRefAlias(typeName: string, refName: string, schema: JSONSchema7, ctx: GoCodegenCtx): void { + if (ctx.generatedNames.has(typeName)) return; + ctx.generatedNames.add(typeName); + + const lines: string[] = []; + if (schema.description) { + pushGoCommentForContext(lines, schema.description, ctx); + } + if (isSchemaExperimental(schema)) { + pushGoExperimentalTypeComment(lines, typeName, ctx); + } + if (isSchemaDeprecated(schema)) { + pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); + } + lines.push(`type ${typeName} = *${goDefinitionName(refName)}`); + ctx.structs.push(lines.join("\n")); +} + function emitGoRpcDefinition(definitionName: string, schema: JSONSchema7, ctx: GoCodegenCtx): string { const typeName = goDefinitionName(definitionName); + + const wireNullableRef = goWireNullableRefName(schema); + if (wireNullableRef) { + emitGoNullableRefAlias(typeName, wireNullableRef, schema, ctx); + return typeName; + } + const effectiveSchema = resolveObjectSchema(schema, ctx.definitions) ?? resolveSchema(schema, ctx.definitions) ?? schema; if (isStringEnumDefinition(effectiveSchema)) { @@ -3216,6 +3265,10 @@ export function generateGoSessionEventsCode( if (variant.dataExperimental || isSchemaExperimental(variant.dataSchema)) { pushGoExperimentalTypeComment(lines, variant.dataClassName, ctx); } + if (variant.eventDeprecated || isSchemaDeprecated(variant.dataSchema)) { + lines.push("//"); + pushGoCommentForContext(lines, `Deprecated: ${variant.dataClassName} is deprecated.`, ctx); + } lines.push(`type ${variant.dataClassName} struct {`); const fields: GoStructField[] = []; @@ -3290,6 +3343,9 @@ export function generateGoSessionEventsCode( if (variant?.eventExperimental) { pushGoExperimentalEventComment(eventTypeEnum, constName, "\t"); } + if (variant?.eventDeprecated) { + pushGoComment(eventTypeEnum, `Deprecated: ${constName} identifies a deprecated event.`, "\t"); + } eventTypeEnum.push(`\t${constName} SessionEventType = "${typeName}"`); } eventTypeEnum.push(`)`); @@ -3533,11 +3589,30 @@ function collectGoTopLevelNames(code: string, keyword: "type" | "const"): string return [...names].sort(compareGoTypeNames); } -function generateGoSessionEventAliasFile( +export function collectGoDeprecatedEventNames(sessionSchema: JSONSchema7): ReadonlySet { + const deprecatedNames = new Set(); + for (const variant of extractGoEventVariants(sessionSchema)) { + if (variant.eventDeprecated || isSchemaDeprecated(variant.dataSchema)) { + deprecatedNames.add(variant.dataClassName); + } + if (variant.eventDeprecated) { + deprecatedNames.add( + "SessionEventType" + variant.typeName + .split(/[._]/) + .map((word) => goIdentifierWord(word)) + .join("") + ); + } + } + return deprecatedNames; +} + +export function generateGoSessionEventAliasFile( generatedSessionTypeCode: string, additionalTypeNames: Iterable = [], additionalConstNames: Iterable = [], - excludeTypeNames: Iterable = [] + excludeTypeNames: Iterable = [], + deprecatedNames: ReadonlySet = new Set() ): string { const excluded = new Set(excludeTypeNames); const typeNames = [...new Set([...collectGoTopLevelNames(generatedSessionTypeCode, "type"), ...additionalTypeNames])] @@ -3559,6 +3634,9 @@ function generateGoSessionEventAliasFile( lines.push(`// Session-event types are generated in the rpc package and aliased here for source compatibility.`); lines.push(`type (`); for (const typeName of typeNames) { + if (deprecatedNames.has(typeName)) { + pushGoComment(lines, `Deprecated: ${typeName} is deprecated.`, "\t"); + } lines.push(`\t${typeName} = rpc.${typeName}`); } lines.push(`)`); @@ -3569,6 +3647,9 @@ function generateGoSessionEventAliasFile( lines.push(`// Session-event constants are generated in the rpc package and re-exported here for source compatibility.`); lines.push(`const (`); for (const constName of constNames) { + if (deprecatedNames.has(constName)) { + pushGoComment(lines, `Deprecated: ${constName} identifies a deprecated event.`, "\t"); + } lines.push(`\t${constName} = rpc.${constName}`); } lines.push(`)`); @@ -3826,9 +3907,10 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema) // aliases that clash with the existing hand-written `copilot.ContextTier`. const handWrittenPublicNames = await collectHandWrittenGoPublicNames(); const aliasExcludes = new Set([...internalTypesInSession, ...handWrittenPublicNames]); + const deprecatedNames = collectGoDeprecatedEventNames(sessionSchema); const aliasOutPath = await writeGeneratedFile( "go/zsession_events.go", - generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames, aliasExcludes) + generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames, aliasExcludes, deprecatedNames) ); console.log(` ✓ ${aliasOutPath}`); @@ -4298,11 +4380,11 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc lines.push(`\t}`); lines.push(`\treturn result, nil`); } else { - lines.push(`\tvar result ${resultType}`); + lines.push(`\tvar result ${nullableInner ? "*" : ""}${resultType}`); lines.push(`\tif err := json.Unmarshal(raw, &result); err != nil {`); lines.push(`\t\treturn nil, err`); lines.push(`\t}`); - lines.push(`\treturn &result, nil`); + lines.push(`\treturn ${nullableInner ? "" : "&"}result, nil`); } lines.push(`}`); lines.push(``); @@ -4588,7 +4670,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro const __filename = fileURLToPath(import.meta.url); -if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { +if (isCodegenEntrypoint(process.argv[1], __filename)) { const sessionArg = process.argv[2] || undefined; const apiArg = process.argv[3] || undefined; generate(sessionArg, apiArg).catch((err) => { diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index af35f314ae..1c66958585 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -7,7 +7,6 @@ */ import fs from "fs/promises"; -import path from "path"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { fileURLToPath } from "url"; import { @@ -18,6 +17,7 @@ import { getApiSchemaPath, getRpcSchemaTypeName, getSessionEventsSchemaPath, + isCodegenEntrypoint, isObjectSchema, isOpaqueJson, isVoidSchema, @@ -114,16 +114,7 @@ function rewriteExternalRefsForPython(schema: JSONSchema7 & { definitions?: Reco if (!schema.definitions) schema.definitions = {}; for (const placeholder of placeholderNames.keys()) { if (!schema.definitions[placeholder]) { - const markerProperty = `__externalRefMarker_${placeholder}`; - schema.definitions[placeholder] = { - type: "object", - additionalProperties: false, - title: placeholder, - properties: { - [markerProperty]: { type: "string" }, - }, - required: [markerProperty], - }; + schema.definitions[placeholder] = pythonReferencePlaceholder(placeholder); } } } @@ -131,6 +122,17 @@ function rewriteExternalRefsForPython(schema: JSONSchema7 & { definitions?: Reco return { placeholderNames, imports }; } +function pythonReferencePlaceholder(placeholder: string): JSONSchema7 { + const markerProperty = `__externalRefMarker_${placeholder}`; + return { + type: "object", + additionalProperties: false, + title: placeholder, + properties: { [markerProperty]: { type: "string" } }, + required: [markerProperty], + }; +} + function placeholderToQuicktypeIdentifier(placeholder: string): string { return placeholder .replace(/^_+/, "") @@ -290,6 +292,52 @@ function preservePythonSessionEventConstructorOrder(schema: JSONSchema7): void { } } +/** + * Optional RPC fields added after a type was first published. Python dataclasses expose + * their field order as a positional constructor contract, so these must stay last instead + * of being sorted in among the pre-existing optional fields. + */ +const PY_RPC_APPEND_LAST_FIELDS: ReadonlyArray = [ + ["ConnectorReconcileRequest", "forceConnectorName"], +]; + +/** + * Append-last entries whose property exists in this schema. An older or narrower schema + * selected for generation may predate the field, which is not an error; a field the + * schema declares but the generated dataclass lacks still fails generation. + */ +export function pythonAppendLastFieldsPresentIn( + definitions: Record, + fields: ReadonlyArray = PY_RPC_APPEND_LAST_FIELDS +): ReadonlyArray { + const result = fields.filter(([className, propertyName]) => { + const definition = definitions[className]; + return typeof definition === "object" && Object.hasOwn(definition.properties ?? {}, propertyName); + }); + const seen = new Set(result.map(([className, propertyName]) => `${className}.${propertyName}`)); + + for (const [className, definition] of Object.entries(definitions)) { + if (typeof definition !== "object") continue; + // Quicktype merges inline union variants into the definition's dataclass. + const shapes = [definition, ...(definition.anyOf ?? definition.oneOf ?? [])]; + for (const [propertyName, property] of shapes.flatMap((shape) => + typeof shape === "object" ? Object.entries(shape.properties ?? {}) : [] + )) { + if ( + typeof property !== "object" || + property["x-copilot-sdk-append-last"] !== true || + seen.has(`${className}.${propertyName}`) + ) { + continue; + } + result.push([className, propertyName]); + seen.add(`${className}.${propertyName}`); + } + } + + return result; +} + function preservePythonRpcStringDateFields(definitions: Record): void { const quotaSnapshot = definitions.AccountQuotaSnapshot; const resetDate = quotaSnapshot?.properties?.resetDate as JSONSchema7 | undefined; @@ -1343,7 +1391,8 @@ function removeRequiredAnyDefaultsForPython( } const requiredFields = resolved.required.map(toSnakeCase); - for (const className of new Set([definitionName, toPascalCase(definitionName)])) { + // Quicktype capitalizes acronyms, for example McpPromptMessage becomes MCPPromptMessage. + for (const className of new Set([definitionName.toLowerCase(), toPascalCase(definitionName).toLowerCase()])) { const fields = requiredFieldsByClass.get(className) ?? new Set(); for (const field of requiredFields) { fields.add(field); @@ -1354,7 +1403,7 @@ function removeRequiredAnyDefaultsForPython( const classBlockRe = /(@dataclass\r?\nclass\s+(\w+):[\s\S]*?)(?=^@dataclass|^class\s+\w|^def\s+\w|\Z)/gm; return code.replace(classBlockRe, (block: string, _classPrefix: string, className: string) => { - const requiredFields = requiredFieldsByClass.get(className); + const requiredFields = requiredFieldsByClass.get(className.toLowerCase()); if (!requiredFields) { return block; } @@ -1521,7 +1570,97 @@ function makePythonDataclassFieldKeywordOnly( return updated; } -function reorderPythonDataclassFields(code: string): string { +/** + * Move optional fields listed in {@link PY_RPC_APPEND_LAST_FIELDS} to the end of their + * dataclass so a field added after publication cannot shift the positional constructor + * contract of the fields that were already there. + */ +export function appendLastPythonRpcConstructorFields( + code: string, + fields: ReadonlyArray = PY_RPC_APPEND_LAST_FIELDS +): string { + const fieldRe = /^ (\w+): .* = .*$/; + const methodRe = /^ (?:@(?:staticmethod|classmethod|property)|(?:async\s+)?def\s+)/; + + let updated = code; + for (const [className, propertyName] of fields) { + const targetField = toSnakeCase(propertyName); + const classBlockRe = new RegExp( + `(@dataclass\\r?\\nclass\\s+${escapeRegExp(className)}:[\\s\\S]*?)(?=^@dataclass|^class\\s+\\w|^def\\s+\\w|(?![\\s\\S]))`, + "m" + ); + let foundClass = false; + updated = updated.replace(classBlockRe, (block: string) => { + foundClass = true; + const lines = block.split("\n"); + const memberStart = lines.findIndex((line, index) => index >= 2 && methodRe.test(line)); + if (memberStart < 0) { + throw new Error(`Missing from_dict constructor for ${className}`); + } + + const groups: string[][] = []; + const preamble: string[] = []; + let current: string[] | undefined; + for (const line of lines.slice(2, memberStart)) { + if (/^ \w+:/.test(line)) { + current = [line]; + groups.push(current); + } else if (current) { + current.push(line); + } else { + preamble.push(line); + } + } + + const targetIndex = groups.findIndex((group) => fieldRe.exec(group[0])?.[1] === targetField); + if (targetIndex < 0) { + throw new Error(`Missing dataclass field ${className}.${targetField}`); + } + if (targetIndex === groups.length - 1) return block; + + const reordered = [ + ...groups.slice(0, targetIndex), + ...groups.slice(targetIndex + 1), + groups[targetIndex], + ].map((group) => { + const trimmed = [...group]; + while (trimmed.length > 1 && trimmed[trimmed.length - 1].trim() === "") trimmed.pop(); + return trimmed; + }); + const fieldOrder = reordered + .map((group) => group[0].match(/^ (\w+):/)?.[1]) + .filter((name): name is string => Boolean(name)); + let foundConstructor = false; + const members = lines + .slice(memberStart) + .join("\n") + .replace( + new RegExp(`return ${escapeRegExp(className)}\\(([^()\\n]*)\\)`), + (_call: string, args: string) => { + const parsed = args.split(",").map((arg) => arg.trim()); + if (parsed.length !== fieldOrder.length || !parsed.every((arg) => fieldOrder.includes(arg))) { + throw new Error(`Unexpected from_dict constructor arguments for ${className}`); + } + foundConstructor = true; + return `return ${className}(${fieldOrder.join(", ")})`; + } + ); + // Reordering fields without rewriting the positional constructor call would + // silently bind every later argument to the wrong field. + if (!foundConstructor) { + throw new Error(`Missing from_dict constructor for ${className}`); + } + + return [...lines.slice(0, 2), ...preamble, ...reordered.flat(), "", members].join("\n"); + }); + if (!foundClass) { + throw new Error(`Missing dataclass ${className}`); + } + } + return updated; +} + +function reorderPythonDataclassFields(code: string, keywordOnlyLastInClass?: string): string { const fieldRe = /^ \w+: (?:Any|bool|int|float|str|dict|list|ClassVar|[A-Z_]\w*|['"][A-Z_]\w*)(?:[^=]*)?(?: = .*)?$/; const methodRe = /^ (?:@(?:staticmethod|classmethod|property)|(?:async\s+)?def\s+)/; @@ -1562,7 +1701,13 @@ function reorderPythonDataclassFields(code: string): string { const required = groups.filter((group) => !group[0].includes(" = ")); const optional = groups.filter((group) => group[0].includes(" = ")); - const reorderedGroups = [...required, ...optional]; + const reorderedGroups = block.startsWith(`@dataclass\nclass ${keywordOnlyLastInClass}:`) + ? [ + ...required, + ...optional.filter((group) => !group[0].includes("kw_only=True")), + ...optional.filter((group) => group[0].includes("kw_only=True")), + ] + : [...required, ...optional]; const changed = reorderedGroups.some((group, index) => group !== groups[index]); if (!changed) { return block; @@ -1627,8 +1772,12 @@ function getMethodResultSchema(method: RpcMethod): JSONSchema7 | undefined { return resolveSchema(method.result, rpcDefinitions) ?? method.result ?? undefined; } -function isPythonObjectResultSchema(schema: JSONSchema7 | undefined): boolean { +export function isPythonObjectResultSchema( + schema: JSONSchema7 | undefined, + definitions: DefinitionCollections = rpcDefinitions, +): boolean { if (!schema) return false; + schema = resolveSchema(schema, definitions) ?? schema; if (isObjectSchema(schema)) return true; const variants = schema.anyOf ?? schema.oneOf; @@ -1636,7 +1785,7 @@ function isPythonObjectResultSchema(schema: JSONSchema7 | undefined): boolean { const nonNullVariants = variants .filter((variant): variant is JSONSchema7 => typeof variant === "object" && variant !== null) - .map((variant) => resolveObjectSchema(variant, rpcDefinitions) ?? resolveSchema(variant, rpcDefinitions) ?? variant) + .map((variant) => resolveObjectSchema(variant, definitions) ?? resolveSchema(variant, definitions) ?? variant) .filter( (variant) => variant.type !== "null" && @@ -1648,7 +1797,7 @@ function isPythonObjectResultSchema(schema: JSONSchema7 | undefined): boolean { ); if (nonNullVariants.length === 1) { - return isPythonObjectResultSchema(nonNullVariants[0]); + return isPythonObjectResultSchema(nonNullVariants[0], definitions); } return nonNullVariants.length > 1 && findPyDiscriminator(nonNullVariants) !== null; @@ -2135,10 +2284,17 @@ function resolvePyNamedUnion( }; } +/** + * Enums whose contract defines `unknown` as "outside the supported vocabulary". The runtime + * contract decodes later values as `unknown` (`#[serde(other)]`); Python does the same instead + * of failing the whole event. + */ +const COLLAPSE_UNKNOWN_PYTHON_ENUMS = new Set(["PermissionApprovalEvaluationReasonCode"]); + function getOrCreatePyEnum( enumName: string, values: string[], - ctx: PyCodegenCtx, + ctx: Pick, description?: string, enumValueDescriptions?: EnumValueDescriptions, deprecated?: boolean, @@ -2171,6 +2327,23 @@ function getOrCreatePyEnum( } lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`); } + if (enumName === "ToolExecutionCompleteFileEditKind") { + lines.push(``); + lines.push(` @classmethod`); + lines.push(` def _missing_(cls, value: object) -> "ToolExecutionCompleteFileEditKind | None":`); + lines.push(` if not isinstance(value, str):`); + lines.push(` return None`); + lines.push(` member = object.__new__(cls)`); + lines.push(` member._name_ = "UNKNOWN"`); + lines.push(` member._value_ = value`); + lines.push(` return cls._value2member_map_.setdefault(value, member)`); + } else if (COLLAPSE_UNKNOWN_PYTHON_ENUMS.has(enumName)) { + if (!values.includes("unknown")) throw new Error(`${enumName} has no "unknown" value to collapse into`); + lines.push(``); + lines.push(` @classmethod`); + lines.push(` def _missing_(cls, value: object) -> "${enumName} | None":`); + lines.push(` return cls.UNKNOWN if isinstance(value, str) else None`); + } ctx.enumsByName.set(enumName, enumName); ctx.enums.push(lines.join("\n")); return enumName; @@ -3156,6 +3329,24 @@ async function generateSessionEvents(schemaPath?: string): Promise { // ── RPC Types ─────────────────────────────────────────────────────────────── +function preserveUnknownPythonEnumValues(code: string, name: string): string { + const declaration = new RegExp(`^class ${name}\\(Enum\\):\\n(?:[ \\t].*\\n|\\n)*`, "m"); + const match = code.match(declaration); + if (!match) throw new Error(`Missing generated ${name} enum`); + const method = ` + @classmethod + def _missing_(cls, value: object) -> ${name} | None: + if not isinstance(value, str): + return None + member = object.__new__(cls) + member._name_ = "UNKNOWN" + member._value_ = value + return cls._value2member_map_.setdefault(value, member) + +`; + return code.replace(declaration, `${match[0].trimEnd()}\n${method}`); +} + async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema7): Promise { console.log("Python: generating RPC types..."); const { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } = await import("quicktype-core"); @@ -3250,6 +3441,30 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema }; const externalRefs = rewriteExternalRefsForPython(singleSchema as JSONSchema7 & { definitions?: Record }); const externalEnumNames = collectPythonExternalEnumNames(sessionEventsSchema, externalRefs.placeholderNames); + // Discovery and session availability are independently versioned despite identical values. + // Hide the discovery enum from quicktype's enum merging, then restore it with our enum emitter. + const distinctEnums: Pick = { + enumsByName: new Map(), + enums: [], + }; + for (const name of ["ConnectorDiscoveryAvailability"]) { + const definition = allDefinitions[name]; + if (!definition) continue; + if (!definition.enum?.every((value): value is string => typeof value === "string")) { + throw new Error(`Expected a string enum for ${name}`); + } + getOrCreatePyEnum( + name, + definition.enum, + distinctEnums, + definition.description, + getEnumValueDescriptions(definition) + ); + const placeholder = `__ExternalRef_${name}`; + (singleSchema.definitions as Record)[name] = pythonReferencePlaceholder(placeholder); + externalRefs.placeholderNames.set(placeholder, name); + externalEnumNames.add(name); + } const externalDiscriminatedUnionNames = collectPythonExternalDiscriminatedUnionNames( sessionEventsSchema, externalRefs.placeholderNames @@ -3296,6 +3511,10 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema ); typesCode = removeRequiredAnyDefaultsForPython(typesCode, allDefinitions, allDefinitionCollections); typesCode = reorderPythonDataclassFields(typesCode); + typesCode = appendLastPythonRpcConstructorFields( + typesCode, + pythonAppendLastFieldsPresentIn(allDefinitions) + ); // Fix bare except: to use Exception (required by ruff/pylint) typesCode = typesCode.replace(/except:/g, "except Exception:"); // Remove unnecessary pass when class has methods (quicktype generates pass for empty schemas) @@ -3311,6 +3530,7 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema externalEnumNames, externalDiscriminatedUnionNames ); + typesCode += `\n\n${distinctEnums.enums.join("\n\n")}\n`; typesCode = removeShadowedSessionEventEnumsForPython( typesCode, externalRefs.imports.get(".session_events") ?? new Set(), @@ -3349,12 +3569,20 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema // Reorder class/enum definitions to resolve forward references. // Quicktype may emit classes before their dependencies are defined. typesCode = reorderPythonForwardRefs(typesCode); + for (const name of ["CustomizationReloadStatus", "CustomizationReloadSubsystem"]) { + if (Object.hasOwn(allDefinitions, name)) { + typesCode = preserveUnknownPythonEnumValues(typesCode, name); + } + } typesCode = makePythonDataclassFieldKeywordOnly( typesCode, "MCPServerConfigHTTP", "oauth_scopes" ); typesCode = applyPythonLegacyParameters(typesCode, allDefinitions); + // Preserve the published positional SQLite capability when adding binary reads. + typesCode = makePythonDataclassFieldKeywordOnly(typesCode, "SessionFSSetProviderCapabilities", "binary"); + typesCode = reorderPythonDataclassFields(typesCode, "SessionFSSetProviderCapabilities"); // Strip quicktype's import block and preamble — we provide our own unified header. // The preamble ends just before the first helper function (e.g. "def from_str") @@ -3432,12 +3660,25 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema rootFieldTypes.set(match[1], match[2]); } } + const nullableAliasTargets = new Map(); + for (const [defName, definition] of Object.entries(allDefinitions)) { + if (!definition || typeof definition !== "object" || !Array.isArray(definition.anyOf)) continue; + const branches = definition.anyOf.filter((branch): branch is JSONSchema7 => typeof branch === "object"); + // Require a real `type: "null"` branch; the `{ "not": {} }` omission sentinel is not a wire null. + if (!branches.some((branch) => branch.type === "null")) continue; + const refBranches = branches.filter((branch) => typeof branch.$ref === "string"); + if (refBranches.length !== 1) continue; + nullableAliasTargets.set(defName, refBranches[0].$ref!.split("/").pop()!); + } for (const defName of Object.keys(allDefinitions)) { const actualName = rootFieldTypes.get(toSnakeCase(defName)); if (actualName) { definitionAliases.set(defName.toLowerCase(), actualName); if (actualName !== defName && !actualTypeNames.has(defName.toLowerCase()) && /^[A-Za-z_]\w*$/.test(defName)) { - publicTypeAliases.set(defName, actualName); + publicTypeAliases.set( + defName, + nullableAliasTargets.get(defName) === actualName ? `${actualName} | None` : actualName + ); } } } @@ -4249,7 +4490,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro const __filename = fileURLToPath(import.meta.url); -if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { +if (isCodegenEntrypoint(process.argv[1], __filename)) { const sessionArg = process.argv[2] || undefined; const apiArg = process.argv[3] || undefined; generate(sessionArg, apiArg).catch((err) => { diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index a9aafa681b..0048ceb53a 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -11,13 +11,13 @@ */ import { execFile } from "child_process"; -import { realpathSync } from "fs"; import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { + isCodegenEntrypoint, addManagedApprovalRequiredToPermissionRequests, type ApiSchema, type DefinitionCollections, @@ -1226,6 +1226,10 @@ function emitRustStruct( // ── Enum emission ─────────────────────────────────────────────────────────── +function preservesUnknownStringValue(enumName: string): boolean { + return enumName === "CustomizationReloadStatus" || enumName === "CustomizationReloadSubsystem"; +} + /** * Claims a string enum name. Two const literals resolving to one name with different values * fail generation, so distinct discriminators can never collapse into one enum. A const whose @@ -1261,7 +1265,9 @@ function emitRustStringEnum( ): void { if (!claimRustStringEnum(enumName, values, false, ctx)) return; + const preserveUnknownValue = enumName === "ToolExecutionCompleteFileEditKind"; const lines: string[] = []; + const preserveUnknown = preserveUnknownValue || preservesUnknownStringValue(enumName); if (description) { for (const line of description.split(/\r?\n/)) { lines.push(`/// ${line}`); @@ -1269,12 +1275,18 @@ function emitRustStringEnum( } pushRustExperimentalDocs(lines, experimental || ctx.experimentalTypeNames.has(enumName)); lines.push( - "#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]", + preserveUnknown + ? "#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]" + : "#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]", ); + if (preserveUnknown) { + lines.push('#[serde(from = "String", into = "String")]'); + } lines.push(`pub enum ${enumName} {`); const usedVariantNames = new Set(); const reservedVariantNames = new Set(["Unknown"]); + const variants: { value: string; name: string }[] = []; for (const value of values) { // Keep the protocol's explicit "unknown" distinct from the serde fallback, // including anonymous enums whose names depend on their containing type. @@ -1285,22 +1297,57 @@ function emitRustStringEnum( reservedVariantNames, value === "unknown" ? "UnknownValue" : undefined, ); + variants.push({ value, name: variantName }); pushRustDoc(lines, enumValueDescriptions?.[value], " "); - if (variantName !== value) { + if (!preserveUnknown && variantName !== value) { lines.push(` #[serde(rename = "${value}")]`); } lines.push(` ${variantName},`); } - // Add a catch-all for forward compatibility. This is also the `Default` - // variant — for wire-protocol enums an unknown/sentinel value is the only - // safe default. - lines.push(" /// Unknown variant for forward compatibility."); - lines.push(" #[default]"); - lines.push(" #[serde(other)]"); - lines.push(" Unknown,"); + lines.push( + preserveUnknownValue + ? " /// An unrecognized operation kind, retaining its wire value." + : " /// Unknown variant for forward compatibility.", + ); + if (preserveUnknown) { + lines.push(" Unknown(String),"); + } else { + // For wire-protocol enums an unknown/sentinel value is the only safe default. + lines.push(" #[default]"); + lines.push(" #[serde(other)]"); + lines.push(" Unknown,"); + } lines.push("}"); + if (preserveUnknown) { + if (!preserveUnknownValue) lines.push(""); + lines.push(`impl Default for ${enumName} {`); + lines.push(' fn default() -> Self { Self::Unknown("unknown".to_owned()) }'); + lines.push("}"); + if (!preserveUnknownValue) lines.push(""); + lines.push(`impl From for ${enumName} {`); + lines.push(" fn from(value: String) -> Self {"); + lines.push(" match value.as_str() {"); + for (const variant of variants) { + lines.push(` ${JSON.stringify(variant.value)} => Self::${variant.name},`); + } + lines.push(" _ => Self::Unknown(value),"); + lines.push(" }"); + lines.push(" }"); + lines.push("}"); + if (!preserveUnknownValue) lines.push(""); + lines.push(`impl From<${enumName}> for String {`); + lines.push(` fn from(value: ${enumName}) -> Self {`); + lines.push(" match value {"); + for (const variant of variants) { + lines.push(` ${enumName}::${variant.name} => ${JSON.stringify(variant.value)}.to_owned(),`); + } + lines.push(` ${enumName}::Unknown(value) => value,`); + lines.push(" }"); + lines.push(" }"); + lines.push("}"); + } ctx.enums.push(lines.join("\n")); } @@ -1345,6 +1392,7 @@ interface EventVariant { dataSchema: JSONSchema7; /** Description of the event */ description?: string; + eventDeprecated: boolean; /** Whether the event definition is experimental. */ eventExperimental: boolean; /** Whether the event data definition is experimental. */ @@ -1393,6 +1441,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataClassName: `${toPascalCase(typeName)}Data`, dataSchema, description: resolvedVariant.description || dataSchema.description, + eventDeprecated: isSchemaDeprecated(resolvedVariant), eventExperimental: isSchemaExperimental(resolvedVariant), dataExperimental: isSchemaExperimental(dataSchema), }; @@ -1411,6 +1460,7 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { { allowUntaggedUnions: true, allowedUnionTypeNames: [ + "HumanResponseRecordedResponse", "ToolExecutionCompleteContent", "ToolExecutionCompleteContentResourceDetails", ], @@ -1457,6 +1507,9 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { ); typeEnumLines.push("pub enum SessionEventType {"); for (const variant of variants) { + if (variant.eventDeprecated) { + typeEnumLines.push(...rustDeprecatedAttributes(" ")); + } pushRustExperimentalDocs( typeEnumLines, variant.eventExperimental, @@ -1484,6 +1537,9 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { dataEnumLines.push(`#[serde(tag = "type", content = "data")]`); dataEnumLines.push("pub enum SessionEventData {"); for (const variant of variants) { + if (variant.eventDeprecated) { + dataEnumLines.push(...rustDeprecatedAttributes(" ")); + } pushRustExperimentalDocs( dataEnumLines, variant.dataExperimental, @@ -1662,6 +1718,14 @@ function rustResultTypeName(method: RpcMethod, ctx: RustCodegenCtx): string { recordExternalRustTypeRef(method.result.$ref, ctx); return rustRefTypeName(method.result.$ref); } + if ( + (method.rpcMethod === "session.instructions.reload" && + method.result?.$ref === "#/definitions/InstructionsGetSourcesResult") || + (method.rpcMethod === "session.customizations.reload" && + method.result?.$ref === "#/definitions/CustomizationsReloadResult") + ) { + return rustRefTypeName(method.result.$ref, ctx.definitions); + } return getRpcSchemaTypeName( method.result, `${toPascalCase(method.rpcMethod)}Result`, @@ -1882,7 +1946,6 @@ export function generateApiTypesCode( allowedUnionTypeNames: [ "AuthInfo", "EnqueueCommandResult", - "EntraTokenAcquireResult", "McpOauthProbeResult", "SettableAuthInfo", "ToolResult", @@ -2003,8 +2066,18 @@ export function generateApiTypesCode( const current = inlineMethodParamSchemas.get(name) ?? (def as JSONSchema7); const legacyRequest = legacyRequests.get(name); const schema = legacyRequest ? rustLegacyStructSchema(current, legacyRequest, name) : current; + const nullableRef = getNullableInner(schema)?.$ref; - if (schema.enum && Array.isArray(schema.enum)) { + if (nullableRef) { + recordExternalRustTypeRef(nullableRef, ctx); + const innerType = rustRefTypeName(nullableRef, defCollections); + emitRustTypeAlias( + name, + schema, + hasWireNullBranch(schema) ? `Option<${innerType}>` : innerType, + ctx, + ); + } else if (schema.enum && Array.isArray(schema.enum)) { emitRustStringEnum( name, schema.enum as string[], @@ -2375,6 +2448,23 @@ function getResultTypeName( return `${toPascalCase(method.rpcMethod)}Result`; } +/** + * A named alias is wire-nullable only when its `anyOf` carries a real + * `type: "null"` branch. `getNullableInner` also accepts the `{ "not": {} }` + * omission sentinel, which means "absent", not "may be null on the wire", so + * aliasing it to `Option` would accept and serialize a null the schema + * does not permit. + */ +function hasWireNullBranch(schema: JSONSchema7): boolean { + if (!Array.isArray(schema.anyOf)) return false; + return schema.anyOf.some( + (branch) => + typeof branch === "object" && + branch !== null && + (branch as JSONSchema7).type === "null", + ); +} + function methodUsesInternalSchema( schema: JSONSchema7 | null | undefined, defCollections: DefinitionCollections, @@ -2799,31 +2889,7 @@ async function generate(): Promise { console.log(`Done! Generated files in ${GENERATED_DIR}`); } -const __filename = fileURLToPath(import.meta.url); - -export function isRustCodegenEntrypoint( - entryPath: string | undefined, - modulePath = __filename, - platform = process.platform, -): boolean { - if (!entryPath) { - return false; - } - const canonicalize = (filePath: string) => { - try { - return realpathSync.native(filePath); - } catch { - return path.resolve(filePath); - } - }; - const canonicalEntryPath = canonicalize(entryPath); - const canonicalModulePath = canonicalize(modulePath); - return platform === "win32" - ? canonicalEntryPath.toLowerCase() === canonicalModulePath.toLowerCase() - : canonicalEntryPath === canonicalModulePath; -} - -if (isRustCodegenEntrypoint(process.argv[1])) { +if (isCodegenEntrypoint(process.argv[1], fileURLToPath(import.meta.url))) { generate().catch((err) => { console.error("Code generation failed:", err); process.exit(1); diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index f1a59faa1b..9cce8743ee 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -7,10 +7,8 @@ */ import fs from "fs/promises"; -import { realpathSync } from "fs"; import type { JSONSchema7 } from "json-schema"; import { compile } from "json-schema-to-typescript"; -import path from "path"; import { fileURLToPath } from "url"; import { getApiSchemaPath, @@ -18,6 +16,7 @@ import { getNullableInner, getRpcSchemaTypeName, getSessionEventsSchemaPath, + isCodegenEntrypoint, postProcessSchema, propagateInternalVisibility, writeGeneratedFile, @@ -386,6 +385,9 @@ export function normalizeSchemaForTypeScript( root.definitions = definitions; delete root.$defs; + const openReloadEnums = new Set( + ["CustomizationReloadStatus", "CustomizationReloadSubsystem"].map((name) => definitions[name]) + ); const internalDefinitionNames = new Set( Object.entries(definitions) .filter(([, definition]) => typeof definition === "object" && definition !== null && isSchemaInternal(definition as JSONSchema7)) @@ -440,14 +442,19 @@ export function normalizeSchemaForTypeScript( } const enumValueDescriptions = getEnumValueDescriptions(rewritten as JSONSchema7); - if (enumValueDescriptions && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { - rewritten.tsType = (rewritten.enum as string[]) + if ((enumValueDescriptions || openReloadEnums.has(source)) && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { + const documentedValues = (rewritten.enum as string[]) .map((entry) => { - const comment = enumValueDescriptions[entry]; + const comment = enumValueDescriptions?.[entry]; const literal = JSON.stringify(entry); return comment ? `${tsDocCommentText(comment)}\n| ${literal}` : `| ${literal}`; }) .join("\n"); + // Preserve future wire values without losing completion for the known edit kinds. + rewritten.tsType = + rewritten.title === "ToolExecutionCompleteFileEditKind" || openReloadEnums.has(source) + ? `${documentedValues}\n| (string & {})` + : documentedValues; delete rewritten.type; delete rewritten.enum; delete rewritten["x-enumDescriptions"]; @@ -671,13 +678,22 @@ function resultTypeName(method: RpcMethod): string { return externalRef?.definitionName ?? getRpcSchemaTypeName(schema, method.rpcMethod.split(".").map(toPascalCase).join("") + "Result"); } -function tsNullableResultTypeName(method: RpcMethod): string | undefined { - const resultSchema = getMethodResultSchema(method); +export function tsNullableResultTypeName( + method: RpcMethod, + resultSchema = getMethodResultSchema(method), +): string | undefined { if (!resultSchema) return undefined; const inner = getNullableInner(resultSchema); if (!inner) return undefined; // Resolve $ref to a type name if (inner.$ref) { + if ( + method.result?.$ref && + resultSchema.title && + resultSchema.anyOf?.some((variant) => typeof variant === "object" && variant.type === "null") + ) { + return resultSchema.title; + } const refName = inner.$ref.split("/").pop(); if (refName) return `${toPascalCase(refName)} | undefined`; } @@ -1012,7 +1028,7 @@ function emitGroup( // sessionId is already stripped from the generated type definition, // so no need for Omit<..., "sessionId"> sigParams.push(`params${optMark}: ${paramsType}`); - bodyArg = "{ sessionId, ...params }"; + bodyArg = "{ ...params, sessionId }"; } else { bodyArg = "{ sessionId }"; } @@ -1327,29 +1343,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro const __filename = fileURLToPath(import.meta.url); -export function isTypeScriptCodegenEntrypoint( - entryPath: string | undefined, - modulePath = __filename, - platform = process.platform, -): boolean { - if (!entryPath) { - return false; - } - const canonicalize = (filePath: string) => { - try { - return realpathSync.native(filePath); - } catch { - return path.resolve(filePath); - } - }; - const canonicalEntryPath = canonicalize(entryPath); - const canonicalModulePath = canonicalize(modulePath); - return platform === "win32" - ? canonicalEntryPath.toLowerCase() === canonicalModulePath.toLowerCase() - : canonicalEntryPath === canonicalModulePath; -} - -if (isTypeScriptCodegenEntrypoint(process.argv[1])) { +if (isCodegenEntrypoint(process.argv[1], __filename)) { const sessionArg = process.argv[2] || undefined; const apiArg = process.argv[3] || undefined; generate(sessionArg, apiArg).catch((err) => { diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 078e45c4b9..5bb3c9f995 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -8,6 +8,7 @@ import { execFile } from "child_process"; import fs from "fs/promises"; +import { realpathSync } from "fs"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; @@ -24,6 +25,32 @@ const __dirname = path.dirname(__filename); /** Root of the copilot-sdk repo */ export const REPO_ROOT = path.resolve(__dirname, "../.."); +/** Recognizes entrypoints reached through Bazel links or Windows path casing. */ +export function isCodegenEntrypoint( + entryPath: string | undefined, + modulePath: string, + platform = process.platform, +): boolean { + if (!entryPath) { + return false; + } + const canonicalize = (filePath: string) => { + try { + return realpathSync.native(filePath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + return path.resolve(filePath); + } + }; + const canonicalEntryPath = canonicalize(entryPath); + const canonicalModulePath = canonicalize(modulePath); + return platform === "win32" + ? canonicalEntryPath.toLowerCase() === canonicalModulePath.toLowerCase() + : canonicalEntryPath === canonicalModulePath; +} + /** Event types to exclude from generation (internal/legacy types) */ export const EXCLUDED_EVENT_TYPES = new Set(["session.import_legacy"]); diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index 7ddd2c136c..d869196ba7 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -457,6 +457,7 @@ async function main() { cwd: DOCS_DIR, ignore: [".validation/**", "node_modules/**", "IMPROVEMENT_PLAN.md"], }); + mdFiles.push("../java/README.md"); console.log(`Found ${mdFiles.length} markdown files\n`); @@ -472,7 +473,15 @@ async function main() { for (const mdFile of mdFiles) { const fullPath = path.join(DOCS_DIR, mdFile); - const content = fs.readFileSync(fullPath, "utf-8"); + let content = fs.readFileSync(fullPath, "utf-8"); + if (mdFile === "../java/README.md") { + // Replace the legacy README smoke with validation of its exact Quick Start. + const quickStart = /^## Quick Start\r?\n[\s\S]*?^```java\r?\n[\s\S]*?^```/m.exec(content); + if (!quickStart) { + throw new Error(`Could not find the Java Quick Start in ${mdFile}`); + } + content = "\n".repeat(content.slice(0, quickStart.index).split("\n").length - 1) + quickStart[0]; + } const blocks = parseMarkdownCodeBlocks(content, mdFile); for (const block of blocks) { diff --git a/scripts/docs-validation/validate.ts b/scripts/docs-validation/validate.ts index cb7b2ae2f6..cfa14b9251 100644 --- a/scripts/docs-validation/validate.ts +++ b/scripts/docs-validation/validate.ts @@ -385,12 +385,10 @@ async function validateJava(): Promise { fs.copyFileSync(path.join(javaDir, file), path.join(srcDir, file)); } - // Read the inherited SDK version from java/sdk/pom.xml - const sdkPomPath = path.join(ROOT_DIR, "java", "sdk", "pom.xml"); + // The required SDK install writes a flattened POM with ${revision} resolved. + const sdkPomPath = path.join(ROOT_DIR, "java", "sdk", ".flattened-pom.xml"); const sdkPomContent = fs.readFileSync(sdkPomPath, "utf-8"); - const versionMatch = sdkPomContent.match( - /[\s\S]*?([^<]+)<\/version>[\s\S]*?<\/parent>/, - ); + const versionMatch = sdkPomContent.match(/([^<]+)<\/version>/); if (!versionMatch) { throw new Error(`Could not read the Java SDK version from ${sdkPomPath}`); } @@ -421,37 +419,13 @@ async function validateJava(): Promise { fs.writeFileSync(path.join(javaDir, "pom.xml"), pomXml); - // First, install the local SDK into the local Maven repo - const pomPath = path.join(ROOT_DIR, "java", "pom.xml"); - try { - execSync(`mvn install -f "${pomPath}" -Dmaven.test.skip=true -q`, { - encoding: "utf-8", - cwd: path.join(ROOT_DIR, "java"), - }); - } catch (err: any) { - // If SDK install fails, all Java snippets fail - const errorMsg = `SDK install failed: ${(err.stderr || err.message || "").slice(0, 200)}`; - for (const file of files) { - const block = manifest.blocks.find( - (b) => b.outputFile === `java/${file}`, - ); - results.push({ - file: `java/${file}`, - sourceFile: block?.sourceFile || "unknown", - sourceLine: block?.sourceLine || 0, - success: false, - errors: [errorMsg], - }); - } - return results; - } - - // Compile the validation project + // The docs task or CI installs the SDK before validation. try { const validationPom = path.join(javaDir, "pom.xml"); - execSync(`mvn compile -f "${validationPom}" -q`, { + const maven = process.platform === "win32" ? "mvnw.cmd" : "./mvnw"; + execSync(`${maven} compile -f "${validationPom}" -q`, { encoding: "utf-8", - cwd: javaDir, + cwd: path.join(ROOT_DIR, "java"), }); // All files passed @@ -487,6 +461,10 @@ async function validateJava(): Promise { } } + if (fileErrors.size === 0) { + throw new Error(`Java documentation compilation failed:\n${output}`); + } + for (const file of files) { const block = manifest.blocks.find( (b) => b.outputFile === `java/${file}`, diff --git a/scripts/install-dependencies.mjs b/scripts/install-dependencies.mjs new file mode 100644 index 0000000000..123e822c08 --- /dev/null +++ b/scripts/install-dependencies.mjs @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +export function installNpmDependencies(directory, runNpm) { + const nodeModules = join(directory, "node_modules"); + const stampFile = join(nodeModules, ".copilot-sdk-install-stamp"); + const args = ["ci", "--ignore-scripts", "--include=dev"]; + const hash = createHash("sha256").update(JSON.stringify(args)); + for (const file of ["package-lock.json", "package.json"]) { + hash.update(readFileSync(join(directory, file))); + } + const fingerprint = hash.digest("hex"); + let stamp; + try { + stamp = readFileSync(stampFile, "utf8").trim(); + } catch (error) { + if (error.code !== "ENOENT") { + throw error; + } + } + if (existsSync(nodeModules) && stamp === fingerprint) { + return false; + } + + // Failed installs must not leave a stamp claiming the dependency tree is current. + rmSync(stampFile, { force: true }); + runNpm(args, directory); + mkdirSync(nodeModules, { recursive: true }); + writeFileSync(stampFile, `${fingerprint}\n`); + return true; +} diff --git a/scripts/run-tasks.mjs b/scripts/run-tasks.mjs index a74fafbf14..8b0e4592b7 100644 --- a/scripts/run-tasks.mjs +++ b/scripts/run-tasks.mjs @@ -10,6 +10,7 @@ import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { bazelActionEnvironmentArgument, prepareSdkSources } from "./build-prerequisites.mjs"; import { findRuntimeRoot, getRuntimeCliPaths } from "./runtime-layout.mjs"; +import { installNpmDependencies } from "./install-dependencies.mjs"; const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const languages = ["nodejs", "python", "go", "dotnet", "java", "rust"]; @@ -35,7 +36,7 @@ const tasks = { "generate:schemas": {}, build: { nodejs: [ - command("nodejs", "npm", ["ci", "--ignore-scripts"]), + command("nodejs", "npm", ["ci", "--ignore-scripts", "--include=dev"]), command("nodejs", "npm", ["run", "build"]), ], python: [ @@ -142,7 +143,15 @@ const tasks = { python: [command("scripts/docs-validation", "npm", ["run", "validate:py"])], go: [command("scripts/docs-validation", "npm", ["run", "validate:go"])], dotnet: [command("scripts/docs-validation", "npm", ["run", "validate:cs"])], - java: [command("scripts/docs-validation", "npm", ["run", "validate:java"])], + java: [ + command("java", maven, [ + "install", + "-Dmaven.test.skip=true", + "-Dskip.test.harness=true", + "-Dcopilot.native.skip.download=true", + ]), + command("scripts/docs-validation", "npm", ["run", "validate:java"]), + ], }, }; @@ -151,6 +160,11 @@ tasks["test:default"] = { rust: tasks.test.rust, }; +tasks["build:default"] = { + nodejs: tasks.build.nodejs, + rust: tasks.build.rust, +}; + tasks.check.rust = [...tasks["format:check"].rust, ...tasks.lint.rust]; function collectGoFiles(directory) { @@ -304,7 +318,7 @@ export function runTasks(verb, language, options = {}) { if ( runtimeSource === "checkout" && runtimeRoot && - ["build", "generate", "generate:schemas", "test", "test:default"].includes(verb) + ["build", "build:default", "generate", "generate:schemas", "test", "test:default"].includes(verb) ) { const selectedLanguages = language ? [language] : Object.keys(tasks[verb]); prepareSdkSources({ languages: selectedLanguages, runtimeRoot, sdkRoot }); @@ -341,13 +355,21 @@ export function runTasks(verb, language, options = {}) { if (step.kind === "gofmt") { runGoFormat(step.write); } else { - if (resolvedRuntimeSource === "checkout" && verb === "build" && step.language === "rust") { + if ( + resolvedRuntimeSource === "checkout" && + (verb === "build" || verb === "build:default") && + step.language === "rust" + ) { runRootCommand( runtimeRoot, "pnpm", ["bazel", "build", bazelActionEnvironmentArgument(environment), "//src/sdk/rust:github-copilot-sdk"], environment, ); + } else if (step.executable === "npm" && step.args[0] === "ci") { + installNpmDependencies(resolve(sdkRoot, step.cwd), (args) => + runCommand({ ...step, args }, environment), + ); } else { runCommand(step, environment); } diff --git a/scripts/runtime-release.mjs b/scripts/runtime-release.mjs new file mode 100644 index 0000000000..fcd3585ca3 --- /dev/null +++ b/scripts/runtime-release.mjs @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Resolve a pinned runtime's public release, preserving the caller's mirror override. + * @param {string} version + * @param {string | null | undefined} [baseUrl] Pass null to ignore ambient mirror settings. + */ +export function runtimeReleaseUrl(version, baseUrl = process.env.COPILOT_CLI_DOWNLOAD_BASE_URL) { + const unstable = + /^(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*)){2}(?:-unstable|-(?:0|[1-9][0-9]*)\.unstable)\.r[1-9][0-9]*\.g[0-9a-f]{7}$/.test( + version, + ); + const repository = unstable ? "copilot-sdk" : "copilot-cli"; + const tag = unstable ? `runtime-${version}` : `v${version}`; + const base = baseUrl ?? `https://github.com/github/${repository}/releases/download`; + return `${base.replace(/\/+$/, "")}/${tag}`; +} diff --git a/test/harness/capturingHttpProxy.ts b/test/harness/capturingHttpProxy.ts index fdc1fc46c1..91fe1d061a 100644 --- a/test/harness/capturingHttpProxy.ts +++ b/test/harness/capturingHttpProxy.ts @@ -119,6 +119,9 @@ export class CapturingHttpProxy { }); }); }); + // Tests pause between control requests; idle expiry can race a pooled POST. + // Fixture teardown already closes every connection explicitly. + this.server.keepAliveTimeout = 0; return new Promise((resolve, reject) => { this.server!.on("error", (err) => { diff --git a/test/harness/certUtils.ts b/test/harness/certUtils.ts index ed1754547a..56be7d41b0 100644 --- a/test/harness/certUtils.ts +++ b/test/harness/certUtils.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +import net from "net"; import tls from "tls"; import forge from "node-forge"; @@ -35,6 +36,7 @@ export function generateCA(): CaData { cert.setExtensions([ { name: "basicConstraints", cA: true, critical: true }, { name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true }, + { name: "subjectKeyIdentifier" }, ]); cert.sign(keys.privateKey, forge.md.sha256.create()); @@ -47,10 +49,34 @@ export function generateCA(): CaData { }; } +export interface IdentityData { + certPem: string; + keyPem: string; +} + export function createSecureContextForHost( hostname: string, ca: CaData, ): tls.SecureContext { + const identity = createIdentityForHost(hostname, ca); + return tls.createSecureContext({ + key: identity.keyPem, + cert: identity.certPem, + ca: ca.certPem, + }); +} + +/** + * Issues a CA-signed server identity for `hostname`. IP literals get an IP + * subjectAltName rather than a DNS one, because verifiers reject a DNS name + * when the client connected to an address; `serverAuth` and modern key-usage + * extensions keep strict platform verifiers (macOS SecTrust) from rejecting + * the chain outright. + */ +export function createIdentityForHost( + hostname: string, + ca: CaData, +): IdentityData { const keys = forge.pki.rsa.generateKeyPair(2048); const cert = forge.pki.createCertificate(); cert.publicKey = keys.publicKey; @@ -65,17 +91,31 @@ export function createSecureContextForHost( cert.setSubject([{ name: "commonName", value: hostname }]); cert.setIssuer(ca.caCert.subject.attributes); cert.setExtensions([ + { name: "basicConstraints", cA: false, critical: true }, + { + name: "keyUsage", + digitalSignature: true, + keyEncipherment: true, + critical: true, + }, + { name: "extKeyUsage", serverAuth: true }, { name: "subjectAltName", - altNames: [{ type: 2, value: hostname }], + altNames: net.isIP(hostname) + ? [{ type: 7, ip: hostname }] + : [{ type: 2, value: hostname }], + }, + { + name: "authorityKeyIdentifier", + keyIdentifier: ca.caCert.generateSubjectKeyIdentifier().getBytes(), }, + { name: "subjectKeyIdentifier" }, ]); cert.sign(ca.caKey, forge.md.sha256.create()); - return tls.createSecureContext({ - key: forge.pki.privateKeyToPem(keys.privateKey), - cert: forge.pki.certificateToPem(cert), - ca: ca.certPem, - }); + return { + keyPem: forge.pki.privateKeyToPem(keys.privateKey), + certPem: forge.pki.certificateToPem(cert), + }; } diff --git a/test/harness/mcp-prompt-fixtures.json b/test/harness/mcp-prompt-fixtures.json new file mode 100644 index 0000000000..41fe307adc --- /dev/null +++ b/test/harness/mcp-prompt-fixtures.json @@ -0,0 +1,129 @@ +{ + "$comment": "Copyright (c) Microsoft Corporation. All rights reserved.", + "firstPage": { + "prompts": [ + { + "name": "rich", + "title": "Rich prompt", + "description": "Ordered opaque content", + "icons": [ + { + "src": "data:image/png;base64,aW1hZ2U=", + "mimeType": "image/png", + "sizes": ["16x16", "32x32"], + "theme": "dark", + "additionalProperties": { + "x-icon": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + } + ], + "_meta": { "descriptor": { "version": 1 } }, + "additionalProperties": { + "x-prompt": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + }, + "arguments": [ + { + "name": "topic", + "description": "Required topic", + "required": true, + "_meta": { "argument": { "label": "Topic" } }, + "additionalProperties": { + "x-argument": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + }, + { "name": "style", "description": "Optional style", "required": false }, + { "name": "extra" } + ] + } + ], + "nextCursor": "page:2/opaque+cursor=", + "_meta": { "page": { "number": 1 } }, + "additionalProperties": { + "x-list": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + }, + "secondPage": { + "prompts": [ + { "name": "echo", "arguments": [] }, + { "name": "refresh", "description": "Publish a list change" } + ], + "_meta": { "page": { "number": 2 } } + }, + "richPrompt": { + "description": "A prompt is data, not a model invocation", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Explain opaque content", + "annotations": { "audience": ["user"], "priority": 0.75 }, + "_meta": { "nested": { "values": [true, null, 3] } }, + "futureField": { "preserve": false } + }, + "_meta": { "message": { "index": 0 } }, + "additionalProperties": { + "x-message": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + }, + { + "role": "assistant", + "content": { "type": "image", "data": "aW1hZ2U=", "mimeType": "image/png" } + }, + { + "role": "user", + "content": { "type": "audio", "data": "YXVkaW8=", "mimeType": "audio/wav" } + }, + { + "role": "assistant", + "content": { + "type": "resource", + "resource": { + "uri": "test://prompt/text", + "mimeType": "text/plain", + "text": "Embedded text", + "_meta": { "origin": { "embedded": true } } + } + } + }, + { + "role": "user", + "content": { + "type": "resource", + "resource": { "uri": "test://prompt/blob", "mimeType": "application/octet-stream", "blob": "AAE=" } + } + }, + { + "role": "assistant", + "content": { + "type": "resource_link", + "uri": "test://prompt/not-fetched", + "name": "Reference", + "title": "Resource link", + "description": "Must not be fetched", + "mimeType": "text/plain", + "size": 42, + "annotations": { "audience": ["assistant"] } + } + }, + { + "role": "user", + "content": { + "type": "future-content", + "payload": { "items": [1, false, null, { "key": "value" }] }, + "_meta": { "extension": ["kept"] } + } + } + ], + "_meta": { "result": { "source": "fixture", "flags": [false, true] } }, + "additionalProperties": { + "x-get": { "nested": [null, false] }, + "additionalProperties": { "literal": false } + } + } +} diff --git a/test/harness/mockHandlers.ts b/test/harness/mockHandlers.ts index 9f75d6819e..8c520a6ff2 100644 --- a/test/harness/mockHandlers.ts +++ b/test/harness/mockHandlers.ts @@ -19,6 +19,9 @@ export function createE2eRequestHandler(options: { } if (targetHost === "github.com") { + if (req.method === "POST" && req.url === "/login/oauth/access_token") { + return forwardToCapiProxy(req, res, options.capiProxyUrl); + } respondJson(res, 404, { message: "Not Found (e2e mock)" }); return true; } diff --git a/test/harness/modelProtocolAdapters.test.ts b/test/harness/modelProtocolAdapters.test.ts index 6773edca80..a7b6ab4ce2 100644 --- a/test/harness/modelProtocolAdapters.test.ts +++ b/test/harness/modelProtocolAdapters.test.ts @@ -303,6 +303,40 @@ describe("Anthropic Messages adapter", () => { }); describe("OpenAI Responses adapter", () => { + test("normalizes image-bearing function output without changing ordinary JSON output", () => { + const imageOutput = JSON.stringify([ + { type: "input_text", text: "Viewed image file successfully." }, + { + type: "input_image", + detail: "auto", + image_url: "data:image/png;base64,AQID", + }, + ]); + const result = JSON.parse( + responsesApiRequestToChatCompletion( + JSON.stringify({ + model: "test-model", + input: [ + { + type: "function_call_output", + call_id: "view-1", + output: imageOutput, + }, + { + type: "function_call_output", + call_id: "other-1", + output: '[{"ok":true}]', + }, + ], + }), + ), + ) as { messages: Array<{ content: string }> }; + expect(result.messages.map((message) => message.content)).toEqual([ + "Viewed image file successfully.", + '[{"ok":true}]', + ]); + }); + test("normalizes messages, binary content, and tools", () => { const result = JSON.parse( responsesApiRequestToChatCompletion( diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 06d5117c96..1092d19f10 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -15,6 +15,8 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import yaml from "yaml"; import { NormalizedData, + type ReplayBackend, + ParsedHttpExchange, ReplayingCapiProxy, ToolResultNormalizer, workingDirPlaceholder, @@ -81,6 +83,43 @@ describe("ReplayingCapiProxy", () => { return yaml.parse(content) as NormalizedData; } + test("does not impose idle expiry on pooled control connections", async () => { + const proxy = new ReplayingCapiProxy("http://localhost"); + const address = await proxy.start(); + const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); + const getExchanges = () => + new Promise<{ + headers: http.IncomingHttpHeaders; + reusedSocket: boolean; + }>((resolve, reject) => { + const request = http.get( + `${address}/exchanges`, + { agent }, + (response) => { + response.on("error", reject); + response.on("end", () => + resolve({ + headers: response.headers, + reusedSocket: request.reusedSocket, + }), + ); + response.resume(); + }, + ); + request.on("error", reject); + }); + + try { + const first = await getExchanges(); + expect(first.headers.connection).toBe("keep-alive"); + expect(first.headers["keep-alive"]).toBeUndefined(); + expect((await getExchanges()).reusedSocket).toBe(true); + } finally { + agent.destroy(); + await proxy.stop(); + } + }); + test("validates registered GitHub identities before replay configuration", async () => { const proxy = new ReplayingCapiProxy("http://localhost"); proxy.setCopilotUserByToken("owner-token", { login: "owner", id: 42 }); @@ -121,6 +160,32 @@ describe("ReplayingCapiProxy", () => { await expect(readFile(outputPath)).rejects.toThrow(/ENOENT/); }); + test("uses configured model display names and resets them between tests", async () => { + const proxy = new ReplayingCapiProxy("http://localhost"); + const address = await proxy.start(); + const config = { filePath: path.join(tempDir, "models.yaml"), workDir }; + try { + await proxy.updateConfig({ + ...config, + modelNames: { "claude-sonnet-5": "Claude Sonnet 5" }, + }); + const named = await fetch(`${address}/models`); + expect(named.status).toBe(200); + expect(await named.json()).toMatchObject({ + data: [{ id: "claude-sonnet-5", name: "Claude Sonnet 5" }], + }); + + await proxy.updateConfig(config); + const reset = await fetch(`${address}/models`); + expect(reset.status).toBe(200); + expect(await reset.json()).toMatchObject({ + data: [{ id: "claude-sonnet-5", name: "claude-sonnet-5" }], + }); + } finally { + await proxy.stop(); + } + }); + test("captures chat completion request and response", async () => { const requestBody = JSON.stringify({ messages: [ @@ -145,6 +210,545 @@ describe("ReplayingCapiProxy", () => { ]); }); + test.each([ + [false, 37], + [true, 37], + [false, 0], + [true, 0], + [false, undefined], + [true, undefined], + [false, null], + [true, null], + ] as const)( + "exposes provider input usage for compaction assertions (streaming: %s, input: %s)", + async (streaming, inputTokens) => { + const summary = "Completed summary"; + const usage = { + prompt_tokens: inputTokens, + completion_tokens: 5, + total_tokens: (inputTokens ?? 0) + 5, + }; + const response = { + id: "compaction-response", + object: streaming ? "chat.completion.chunk" : "chat.completion", + created: 1, + model: "test-model", + choices: [ + { + index: 0, + ...(streaming + ? { delta: { role: "assistant", content: summary } } + : { message: { role: "assistant", content: summary } }), + finish_reason: "stop", + logprobs: null, + }, + ], + usage, + }; + const proxy = new ReplayingCapiProxy( + "http://localhost", + path.join(tempDir, "compaction.yaml"), + workDir, + ); + (proxy.exchanges as Array).push({ + request: { + url: "/chat/completions", + method: "POST", + body: JSON.stringify({ messages: [], model: "test-model" }), + headers: { + "x-interaction-type": "conversation-compaction", + "x-interaction-id": "compaction-interaction", + }, + }, + response: { + statusCode: 200, + body: streaming + ? `data: ${JSON.stringify(response)}\n\ndata: [DONE]\n\n` + : JSON.stringify(response), + }, + }); + const address = await proxy.start(); + try { + const result = await fetch(`${address}/exchanges`); + expect(result.ok).toBe(true); + const exchanges = (await result.json()) as ParsedHttpExchange[]; + const compactionResponses = exchanges.filter( + (exchange) => + exchange.response?.choices && + exchange.requestHeaders?.["x-interaction-type"] === + "conversation-compaction", + ); + expect(compactionResponses).toHaveLength(1); + expect( + compactionResponses[0].response?.choices + .map((choice) => choice.message.content ?? "") + .join(""), + ).toBe(summary); + expect( + compactionResponses[0].response?.usage?.prompt_tokens ?? undefined, + ).toBe(inputTokens ?? undefined); + expect(compactionResponses[0].compactionUsage).toEqual({ + interactionId: "compaction-interaction", + summary, + responseCount: 1, + ...(inputTokens == null ? {} : { inputTokens }), + }); + } finally { + await proxy.stop(true); + } + }, + ); + + test.each([ + [ + "reasoning-only", + null, + 11, + "Completed summary", + 37, + true, + 48, + ], + [ + "split text", + "", + 11, + "Completed summary", + 37, + true, + 48, + true, + ], + [ + "missing first input", + null, + undefined, + "Completed summary", + 37, + true, + undefined, + ], + [ + "null final input", + null, + 11, + "Completed summary", + null, + true, + undefined, + ], + [ + "explicit zero", + null, + 0, + "Completed summary", + 0, + true, + 0, + ], + [ + "new attempt", + null, + 11, + "Completed summary", + 37, + false, + 37, + ], + ] as const)( + "correlates compaction provider usage across %s", + async ( + _name, + firstContent, + firstInput, + finalContent, + finalInput, + continuation, + expectedInput, + multipartContinuation: boolean = false, + ) => { + const proxy = new ReplayingCapiProxy( + "http://localhost", + path.join(tempDir, "compaction.yaml"), + workDir, + ); + const responses = [ + { content: firstContent, inputTokens: firstInput }, + { content: finalContent, inputTokens: finalInput }, + ]; + for (const [index, { content, inputTokens }] of responses.entries()) { + (proxy.exchanges as Array).push({ + request: { + url: "/chat/completions", + method: "POST", + body: JSON.stringify({ + messages: [ + { + role: "user", + content: + index === 1 && continuation + ? multipartContinuation + ? [ + { + type: "text", + text: "clock\n", + }, + { + type: "text", + text: "Please continue from where you left off.", + }, + ] + : "Please continue from where you left off." + : "Summarize the conversation.", + }, + ], + model: "test-model", + }), + headers: { + "x-interaction-type": "conversation-compaction", + "x-interaction-id": "compaction-interaction", + }, + }, + response: { + statusCode: 200, + body: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + content, + reasoning_content: + content === null ? "Reasoning before summary" : undefined, + }, + finish_reason: + index === 0 && content !== null ? "length" : "stop", + }, + ], + usage: { prompt_tokens: inputTokens, completion_tokens: 5 }, + }), + }, + }); + if (index === 0) { + for (const [interactionType, interactionId, statusCode] of [ + ["conversation", "compaction-interaction", 200], + ["conversation-compaction", "other-interaction", 200], + ["conversation-compaction", "compaction-interaction", 500], + ]) { + (proxy.exchanges as Array).push({ + request: { + url: "/chat/completions", + method: "POST", + body: JSON.stringify({ messages: [], model: "test-model" }), + headers: { + "x-interaction-type": interactionType, + "x-interaction-id": interactionId, + }, + }, + response: { + statusCode, + body: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + content: "Unrelated response", + }, + }, + ], + usage: { prompt_tokens: 999 }, + }), + }, + }); + } + } + } + const address = await proxy.start(); + try { + const result = await fetch(`${address}/exchanges`); + expect(result.ok).toBe(true); + const exchanges = (await result.json()) as ParsedHttpExchange[]; + const compactions = exchanges.filter( + (exchange) => + exchange.compactionUsage?.summary === + "Completed summary", + ); + expect(compactions).toHaveLength(1); + expect(compactions[0].compactionUsage).toEqual({ + interactionId: "compaction-interaction", + summary: "Completed summary", + responseCount: continuation ? 2 : 1, + ...(expectedInput === undefined + ? {} + : { inputTokens: expectedInput }), + }); + } finally { + await proxy.stop(true); + } + }, + ); + + test.each(["object", "scalar", "freeform"] as const)( + "preserves opaque tool text while normalizing and replaying paths (%s)", + async (argumentKind) => { + const opaqueText = String.raw`word\b before\nafter ${workingDirPlaceholder}`; + const patchBody = [ + String.raw`+const pattern = /word\b/;`, + String.raw`+const text = "before\nafter";`, + String.raw`+const escapes = "\r\t\b\u0041";`, + `+const root = "${workDir}\\literal\\b";`, + `+const placeholder = "${workingDirPlaceholder}";`, + String.raw`+*** Add File: body\must\stay.txt`, + ].join("\n"); + const patch = [ + "*** Begin Patch", + `*** Add File: ${workDir}\\src\\marker.txt`, + patchBody, + String.raw`*** Update File: src\before.txt`, + String.raw`*** Move to: src\after.txt`, + "@@", + "-before", + "+after", + String.raw`*** Delete File: src\obsolete.txt`, + "*** End Patch", + ].join("\n"); + const expectedPatch = (root: string) => + [ + "*** Begin Patch", + `*** Add File: ${root}/src/marker.txt`, + patchBody, + "*** Update File: src/before.txt", + "*** Move to: src/after.txt", + "@@", + "-before", + "+after", + "*** Delete File: src/obsolete.txt", + "*** End Patch", + ].join("\n"); + const patchArguments = (input: string, filename: string) => + argumentKind === "object" + ? JSON.stringify({ input, path: filename, content: opaqueText }) + : argumentKind === "scalar" + ? JSON.stringify(input) + : input; + const textArguments = (root: string, separator: string) => + JSON.stringify({ + input: opaqueText, + content: opaqueText, + prompt: opaqueText, + paths: [ + `src${separator}one.txt`, + `${root}${separator}src${separator}two.txt`, + ], + }); + const request = { + model: "test-model", + messages: [{ role: "user", content: "Apply the patch" }], + }; + const outputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify(request), + responseBody: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + tool_calls: [ + { + id: "patch-call", + type: "function", + function: { + name: "apply_patch", + arguments: patchArguments(patch, "src\\marker.txt"), + }, + }, + { + id: "text-call", + type: "function", + function: { + name: "literal_tool", + arguments: textArguments(workDir, "\\"), + }, + }, + { + id: "scalar-call", + type: "function", + function: { + name: "scalar_tool", + arguments: JSON.stringify(opaqueText), + }, + }, + ], + }, + }, + ], + }), + }, + ]); + + const result = await readYamlOutput(outputPath); + expect( + result.conversations[0].messages[1].tool_calls?.map( + (call) => call.function?.arguments, + ), + ).toEqual([ + patchArguments(expectedPatch(workingDirPlaceholder), "src/marker.txt"), + textArguments(workingDirPlaceholder, "/"), + JSON.stringify(opaqueText), + ]); + + const proxy = new ReplayingCapiProxy("http://localhost:1"); + await proxy.updateConfig({ + filePath: outputPath, + workDir, + backend: "capi", + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + const response = await fetch(`${proxyUrl}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ...request, stream: false }), + }); + expect(response.status).toBe(200); + const completion = (await response.json()) as ChatCompletion; + expect( + completion.choices[0].message.tool_calls?.map((call) => + call.type === "function" ? call.function.arguments : undefined, + ), + ).toEqual([ + patchArguments(expectedPatch(workDir), "src/marker.txt"), + textArguments(workDir, "/"), + JSON.stringify(opaqueText), + ]); + } finally { + await proxy.stop(true); + } + }, + ); + + test.each(["/", "\\"])( + "normalizes and replays delegated prompts across working directories (%s)", + async (separator) => { + const promptForDir = (root: string, pathSeparator: string) => + `Read ${root}${pathSeparator}src${pathSeparator}marker.txt`; + const taskArguments = (prompt: string) => + JSON.stringify({ + agent_type: "explore", + description: "Read marker.txt", + prompt, + }); + const parentRequest = { + model: "test-model", + messages: [{ role: "user", content: "Delegate the file read" }], + }; + const outputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify(parentRequest), + responseBody: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + tool_calls: [ + { + id: "task-call", + type: "function", + function: { + name: "task", + arguments: taskArguments( + promptForDir(workDir, separator), + ), + }, + }, + ], + }, + }, + ], + }), + }, + { + url: "/chat/completions", + requestBody: JSON.stringify({ + model: "test-model", + messages: [ + { role: "user", content: promptForDir(workDir, separator) }, + ], + }), + responseBody: JSON.stringify({ + choices: [ + { + message: { + role: "assistant", + content: "Delegated file read completed.", + }, + }, + ], + }), + }, + ]); + const result = await readYamlOutput(outputPath); + const normalizedPrompt = promptForDir(workingDirPlaceholder, "/"); + expect(result.conversations).toHaveLength(2); + expect( + result.conversations[0].messages[1].tool_calls?.[0].function?.arguments, + ).toBe(taskArguments(normalizedPrompt)); + expect(result.conversations[1].messages[0].content).toBe( + normalizedPrompt, + ); + + const replayWorkDir = path.join(tempDir, "replay-work"); + const proxy = new ReplayingCapiProxy("http://localhost:1"); + await proxy.updateConfig({ + filePath: outputPath, + workDir: replayWorkDir, + backend: "capi", + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + const parentResponse = await fetch(`${proxyUrl}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ...parentRequest, stream: false }), + }); + expect(parentResponse.status).toBe(200); + const parentCompletion = + (await parentResponse.json()) as ChatCompletion; + const taskCall = parentCompletion.choices[0].message.tool_calls?.[0]; + expect(taskCall?.type).toBe("function"); + if (taskCall?.type !== "function") { + throw new Error("Expected a delegated task tool call"); + } + expect(taskCall.function.name).toBe("task"); + expect(taskCall.function.arguments).toBe( + taskArguments(promptForDir(replayWorkDir, "/")), + ); + const { prompt } = JSON.parse(taskCall.function.arguments) as { + prompt: string; + }; + const childResponse = await fetch(`${proxyUrl}/chat/completions`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "test-model", + messages: [{ role: "user", content: prompt }], + stream: false, + }), + }); + expect(childResponse.status).toBe(200); + const childCompletion = (await childResponse.json()) as ChatCompletion; + expect(childCompletion.choices[0].message.content).toBe( + "Delegated file read completed.", + ); + } finally { + await proxy.stop(true); + } + }, + ); + test("normalizes tool call IDs to sequential values", async () => { const requestBody = JSON.stringify({ messages: [{ role: "user", content: "Do something" }], @@ -360,6 +964,70 @@ describe("ReplayingCapiProxy", () => { expect(result.conversations[0].messages[0].content).toBe("What is 2+2?"); }); + test("strips mode_changed_notice from user messages", async () => { + const requestBody = JSON.stringify({ + messages: [ + { + role: "user", + content: + "Context before.\n\n\nWrite a plan only.\n\n\nCreate a brief implementation plan.", + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Plan ready" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + expect(result.conversations[0].messages[0].content).toBe( + "Context before.\n\nCreate a brief implementation plan.", + ); + }); + + test("drops notice-only user turns while preserving genuinely empty input", async () => { + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Ready" } }], + }); + const noticeOutputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify({ + messages: [ + { + role: "user", + content: + "\nPlan mode is no longer active.\n", + }, + ], + }), + responseBody, + }, + ]); + const noticeResult = await readYamlOutput(noticeOutputPath); + expect(noticeResult.conversations[0].messages).toEqual([ + { role: "assistant", content: "Ready" }, + ]); + + const emptyOutputPath = await createProxy([ + { + url: "/chat/completions", + requestBody: JSON.stringify({ + messages: [{ role: "user", content: "" }], + }), + responseBody, + }, + ]); + const emptyResult = await readYamlOutput(emptyOutputPath); + expect(emptyResult.conversations[0].messages).toEqual([ + { role: "user" }, + { role: "assistant", content: "Ready" }, + ]); + }); + test("strips plan mode prefix from user messages", async () => { const requestBody = JSON.stringify({ messages: [ @@ -692,10 +1360,57 @@ Always include PINEAPPLE_COCONUT_42. expect(toolMessage?.content).toBe(stableResult); }); - test("normalizes read_agent result metadata", async () => { + test.each(["", ", model: claude-sonnet-5"])( + "normalizes read_agent result metadata%s", + async (model) => { + const requestBody = JSON.stringify({ + messages: [ + { role: "user", content: "Help me" }, + { + role: "assistant", + tool_calls: [ + { + id: "tc1", + type: "function", + function: { + name: "read_agent", + arguments: '{"agent_id":"read-file","wait":true}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "tc1", + content: `Agent is idle (waiting for messages). agent_id: read-file, agent_type: explore, status: idle, description: Reading subagent-test.txt, elapsed: 1.25s, total_turns: 1${model}\n\n[Turn 0]\nDone.`, + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "Done" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + const toolMessage = result.conversations[0].messages.find( + (m) => m.role === "tool", + ); + expect(toolMessage?.content).toBe( + "Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s\n\nDone.", + ); + }, + ); + + test("names runtime agent IDs that no task call introduced", async () => { + const runtimeAgentId = "3e0c7565-6091-58cb-85bb-6cb14db23ef7"; + const agentResult = (agentId: string) => + `Agent completed. agent_id: ${agentId}, agent_type: general-purpose, status: completed, description: Probe, elapsed: 0s, total_turns: 0, duration: 0s\n\nDone.`; const requestBody = JSON.stringify({ messages: [ - { role: "user", content: "Help me" }, + { role: "user", content: "Check the agent" }, { role: "assistant", tool_calls: [ @@ -704,7 +1419,10 @@ Always include PINEAPPLE_COCONUT_42. type: "function", function: { name: "read_agent", - arguments: '{"agent_id":"read-file","wait":true}', + arguments: JSON.stringify({ + agent_id: runtimeAgentId, + since_turn: 0, + }), }, }, ], @@ -712,8 +1430,7 @@ Always include PINEAPPLE_COCONUT_42. { role: "tool", tool_call_id: "tc1", - content: - "Agent is idle (waiting for messages). agent_id: read-file, agent_type: explore, status: idle, description: Reading subagent-test.txt, elapsed: 1.25s, total_turns: 1\n\n[Turn 0]\nDone.", + content: agentResult(runtimeAgentId), }, ], }); @@ -726,12 +1443,12 @@ Always include PINEAPPLE_COCONUT_42. ]); const result = await readYamlOutput(outputPath); - const toolMessage = result.conversations[0].messages.find( - (m) => m.role === "tool", - ); - expect(toolMessage?.content).toBe( - "Agent completed. agent_id: read-file, agent_type: explore, status: completed, description: Reading subagent-test.txt, elapsed: 0s, total_turns: 0, duration: 0s\n\nDone.", - ); + const [, readCall, readResult] = result.conversations[0].messages; + expect(JSON.parse(readCall.tool_calls![0].function!.arguments!)).toEqual({ + agent_id: "api-agent", + since_turn: 0, + }); + expect(readResult.content).toBe(agentResult("api-agent")); }); test("normalizes GitHub CLI proxy auth failures", async () => { @@ -832,6 +1549,103 @@ Always include PINEAPPLE_COCONUT_42. }); } + test.each([ + ["scripts/check.cjs", "references/policy.txt"], + ["references/policy.txt", "scripts/check.cjs"], + ])( + "replays OTel skill resource fixtures independently of enumeration order %s %s", + async (first, second) => { + const cachePath = path.join(tempDir, "skill-order.yaml"); + const context = (root: string, files: string[]) => + ` +Base directory for this skill: ${root} + +Related files (use view tool to read): +${files.map((file) => ` - ${root}/${file}`).join("\n")} + +Follow the user's explicit instructions. +Related files (use view tool to read): + - authored-second + - authored-first +`; + await writeFile( + cachePath, + yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { + role: "user", + content: + "\nWrite a plan only.\n\n\n" + + context(`${workingDirPlaceholder}/skills/review`, [ + first, + second, + ]), + }, + { role: "assistant", content: "OTEL_RESOURCE_REPLAY_DONE" }, + ], + }, + ], + } satisfies NormalizedData), + ); + const proxy = new ReplayingCapiProxy("http://localhost"); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + try { + const request = { + model: "test-model", + messages: [ + { + role: "user", + content: + context(path.join(workDir, "skills", "review"), [ + second, + first, + ]) + + "\n\n\nPlan mode is no longer active.\n", + }, + ], + }; + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: request, + }); + expect(response.status).toBe(200); + expect(JSON.parse(response.body).choices[0].message.content).toBe( + "OTEL_RESOURCE_REPLAY_DONE", + ); + + for (const content of [ + context(path.join(workDir, "skills", "review"), [first]), + context(path.join(workDir, "skills", "review"), [ + first, + second, + second, + ]), + request.messages[0].content.replace( + " - authored-second\n - authored-first", + " - authored-first\n - authored-second", + ), + ]) { + const mismatch = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [{ role: "user", content }], + }, + }); + expect(mismatch.status).toBe(500); + } + } finally { + await proxy.stop(true); + } + }, + ); + test("replay-only mode rejects cache misses without contacting the upstream", async () => { let upstreamRequests = 0; const upstream = http.createServer((_request, response) => { @@ -883,6 +1697,107 @@ Always include PINEAPPLE_COCONUT_42. } }); + test.each([ + "capi", + "openai-completions", + "openai-responses", + "anthropic-messages", + ])( + "replays a notice-only stored turn while preserving empty user input through %s", + async (backend) => { + const cachePath = path.join(tempDir, `mode-notice-${backend}.yaml`); + await writeFile( + cachePath, + yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { + role: "user", + content: + "\nPlan mode is no longer active.\n", + }, + { role: "assistant", content: "Notice ready" }, + ], + }, + { + messages: [ + { role: "user" }, + { role: "assistant", content: "Empty ready" }, + ], + }, + ], + } satisfies NormalizedData), + ); + const proxy = new ReplayingCapiProxy("http://localhost:9999"); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + backend, + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + const request = (content: string) => { + switch (backend) { + case "capi": + case "openai-completions": + return { + endpoint: "/chat/completions", + body: { + model: "test-model", + messages: [{ role: "user", content }], + }, + }; + case "openai-responses": + return { + endpoint: "/responses", + body: { + model: "test-model", + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: content }], + }, + ], + }, + }; + case "anthropic-messages": + return { + endpoint: "/v1/messages", + body: { + model: "test-model", + max_tokens: 100, + messages: [{ role: "user", content }], + }, + }; + } + }; + + try { + const noticeRequest = request( + "\nPlan mode is no longer active.\n", + ); + const response = await makeRequest(proxyUrl, noticeRequest.endpoint, { + body: noticeRequest.body, + }); + + expect(response.status).toBe(200); + expect(response.body).toContain("Notice ready"); + + const emptyRequest = request(""); + const emptyResponse = await makeRequest(proxyUrl, emptyRequest.endpoint, { + body: emptyRequest.body, + }); + expect(emptyResponse.status).toBe(200); + expect(emptyResponse.body).toContain("Empty ready"); + } finally { + await proxy.stop(true); + } + }, + ); + test.each([ ["should_accept_blob_attachments", "pixel.png"], ["vision_disabled_then_enabled_via_setmodel", "test.png"], @@ -1080,6 +1995,127 @@ Always include PINEAPPLE_COCONUT_42. } }); + test("replays a CAPI view-image capture for BYOK tool-result and image-turn continuations", async () => { + const cachePath = path.join(tempDir, "view-image.yaml"); + await writeFile( + cachePath, + yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "View the image" }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_0", + type: "function", + function: { + name: "view", + arguments: '{"path":"/image.png"}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "toolcall_0", + content: "Viewed image file successfully.", + }, + { role: "user", content: "Image file at path /image.png\n[image]" }, + { role: "assistant", content: "Image viewed" }, + ], + }, + ], + } satisfies NormalizedData), + ); + const proxy = new ReplayingCapiProxy("http://localhost:9999"); + await proxy.updateConfig({ + filePath: cachePath, + workDir, + backend: "openai-completions", + replayOnly: true, + }); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "System prompt" }, + { role: "user", content: "View the image" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { name: "view", arguments: '{"path":"/image.png"}' }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + content: "Viewed image file successfully.", + }, + ], + }, + }); + + expect(response.status).toBe(200); + expect((JSON.parse(response.body) as ChatCompletion).choices[0].message.content).toBe( + "Image viewed", + ); + + const visionResponse = await makeRequest( + proxyUrl, + "/chat/completions", + { + body: { + model: "test-model", + messages: [ + { role: "system", content: "System prompt" }, + { role: "user", content: "View the image" }, + { + role: "assistant", + tool_calls: [ + { + id: "runtime-call-id", + type: "function", + function: { + name: "view", + arguments: '{"path":"/image.png"}', + }, + }, + ], + }, + { + role: "tool", + tool_call_id: "runtime-call-id", + content: "Viewed image file successfully.", + }, + { + role: "user", + content: "Image file at path /image.png\n[image]", + }, + ], + }, + }, + ); + expect(visionResponse.status).toBe(200); + expect( + (JSON.parse(visionResponse.body) as ChatCompletion).choices[0].message + .content, + ).toBe("Image viewed"); + } finally { + await proxy.stop(true); + } + }); + test("matches shell tool results with shell ID completion markers", async () => { const originalShellConfig = process.platform === "win32" @@ -1660,6 +2696,94 @@ Always include PINEAPPLE_COCONUT_42. } }); + test("replays reads of an agent no task call started with its runtime ID", async () => { + const cachePath = path.join(tempDir, "cache.yaml"); + const agentResult = (agentId: string) => + `Agent completed. agent_id: ${agentId}, agent_type: general-purpose, status: completed, description: Probe, elapsed: 0s, total_turns: 0, duration: 0s\n\nDone.`; + const readCall = (id: string, agentId: string) => ({ + role: "assistant" as const, + tool_calls: [ + { + id, + type: "function" as const, + function: { + name: "read_agent", + arguments: JSON.stringify({ agent_id: agentId, since_turn: 0 }), + }, + }, + ], + }); + const cacheContent = yaml.stringify({ + models: ["test-model"], + conversations: [ + { + messages: [ + { role: "system", content: "${system}" }, + { role: "user", content: "Check the agent" }, + readCall("toolcall_0", "api-agent"), + { + role: "tool", + tool_call_id: "toolcall_0", + content: agentResult("api-agent"), + }, + { + role: "assistant", + tool_calls: [ + { + id: "toolcall_1", + type: "function", + function: { + name: "write_agent", + arguments: '{"agent_id":"api-agent","message":"Continue"}', + }, + }, + ], + }, + ], + }, + ], + } satisfies NormalizedData); + await writeFile(cachePath, cacheContent); + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + // A different ID than any recording saw, as each run generates its own. + const runtimeAgentId = "8d0a3f62-1b4e-4c9a-9f57-2e6b0c1d7a45"; + + try { + const response = await makeRequest(proxyUrl, "/chat/completions", { + body: { + model: "test-model", + messages: [ + { role: "system", content: "Be helpful" }, + { role: "user", content: "Check the agent" }, + readCall("runtime-call-id", runtimeAgentId), + { + role: "tool", + tool_call_id: "runtime-call-id", + content: agentResult(runtimeAgentId), + }, + ], + }, + }); + + expect(response.status).toBe(200); + const parsed = JSON.parse(response.body) as ChatCompletion; + const toolCall = parsed.choices[0].message + .tool_calls![0] as ChatCompletionMessageFunctionToolCall; + expect(JSON.parse(toolCall.function.arguments)).toEqual({ + agent_id: runtimeAgentId, + message: "Continue", + }); + } finally { + await proxy.stop(); + } + }); + test("matches parallel tool results regardless of arrival order", async () => { const cachePath = path.join(tempDir, "cache.yaml"); const cacheContent = yaml.stringify({ @@ -1802,7 +2926,9 @@ Always include PINEAPPLE_COCONUT_42. "magic_number", ]); expect(calls.map((call) => call.index)).toEqual([0, 1]); - expect(JSON.parse(calls[1].function!.arguments!)).toEqual({ seed: "hello" }); + expect(JSON.parse(calls[1].function!.arguments!)).toEqual({ + seed: "hello", + }); expect(chunks.at(-1)?.choices[0].finish_reason).toBe("tool_calls"); } finally { await proxy.stop(); diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 15dc18a7d3..669cd65a6e 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -123,6 +123,8 @@ const defaultModel = "claude-sonnet-5"; */ export class ReplayingCapiProxy extends CapturingHttpProxy { private state: ReplayingCapiProxyState | null = null; + private memoryApiStub: MemoryApiStub | undefined; + private entraLogin: { subjectToken: string; githubToken: string } | undefined; private startPromise: Promise | null = null; private defaultToolResultNormalizers: ToolResultNormalizer[] = [ { toolName: "*", normalizer: normalizeLargeOutputFilepaths }, @@ -201,6 +203,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { filePath: config.filePath, workDir: config.workDir, testInfo: config.testInfo, + modelNames: config.modelNames, backend: parseReplayBackend(config.backend), replayOnly: config.replayOnly === true, autoResponseIndex: 0, @@ -292,6 +295,75 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { return; } + if ( + options.requestOptions.path === "/memory-api-config" && + options.requestOptions.method === "POST" + ) { + this.memoryApiStub = JSON.parse(options.body!) as MemoryApiStub; + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + + if ( + options.requestOptions.path === "/entra-login-config" && + options.requestOptions.method === "POST" + ) { + this.entraLogin = JSON.parse(options.body!) as { + subjectToken: string; + githubToken: string; + }; + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + if ( + this.entraLogin && + options.requestOptions.path === "/entra-broker" + ) { + options.onResponseStart(200, { "content-type": "application/json" }); + options.onData( + Buffer.from( + JSON.stringify({ + accessToken: this.entraLogin.subjectToken, + expiresOnTimestamp: Date.now() + 3600000, + username: "entra-sdk@example.test", + accountId: "entra-sdk-account", + }), + ), + ); + options.onResponseEnd(); + return; + } + if ( + this.entraLogin && + options.requestOptions.path === "/login/oauth/access_token" + ) { + const form = new URLSearchParams(options.body ?? ""); + const valid = + form.get("grant_type") === + "urn:ietf:params:oauth:grant-type:token-exchange" && + form.get("subject_token") === this.entraLogin.subjectToken; + options.onResponseStart(valid ? 200 : 400, { + "content-type": "application/json", + }); + options.onData( + Buffer.from( + JSON.stringify( + valid + ? { + access_token: this.entraLogin.githubToken, + expires_in: 3600, + token_type: "bearer", + } + : { error: "invalid_request" }, + ), + ), + ); + options.onResponseEnd(); + return; + } + // Handle /config endpoint for updating proxy configuration if ( options.requestOptions.path === "/config" && @@ -324,20 +396,22 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { options.requestOptions.method === "GET" ) { const protocol = replayProtocols[this.state?.backend ?? "capi"]; + const modelExchanges = this.exchanges.filter( + (exchange) => exchange.request.url === protocol.endpoint, + ); const parsedExchanges = await Promise.all( - this.exchanges - .filter((exchange) => exchange.request.url === protocol.endpoint) - .map((exchange) => - parseHttpExchange( - protocol.normalizeRequest?.(exchange.request.body) ?? - exchange.request.body, - protocol.canonicalResponse - ? exchange.response?.body - : undefined, - exchange.request.headers, - ), + modelExchanges.map((exchange) => + parseHttpExchange( + protocol.normalizeRequest?.(exchange.request.body) ?? + exchange.request.body, + protocol.canonicalResponse + ? exchange.response?.body + : undefined, + exchange.request.headers, ), + ), ); + addCompactionProviderUsage(parsedExchanges, modelExchanges); options.onResponseStart(200, {}); options.onData(Buffer.from(JSON.stringify(parsedExchanges))); options.onResponseEnd(); @@ -432,7 +506,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { state.storedData?.models && state.storedData.models.length > 0 ? state.storedData.models : [defaultModel]; - const modelsResponse = createGetModelsResponse(models); + const modelsResponse = createGetModelsResponse(models, state.modelNames); const body = JSON.stringify(modelsResponse); const headers = { "content-type": "application/json", @@ -516,8 +590,14 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { // Matches: /agents/*/memory/*/enabled, /agents/*/memory/*/recent, etc. if (options.requestOptions.path?.match(/\/agents\/.*\/memory\//)) { let body: string; + let statusCode = 200; if (options.requestOptions.path.includes("/enabled")) { - body = JSON.stringify({ enabled: false }); + body = JSON.stringify(this.memoryApiStub?.enabled ?? { enabled: false }); + } else if (options.requestOptions.path.includes("/memories")) { + statusCode = this.memoryApiStub?.memoriesStatusCode ?? 200; + body = JSON.stringify( + statusCode === 404 ? {} : (this.memoryApiStub?.memories ?? {}), + ); } else if (options.requestOptions.path.includes("/recent")) { body = JSON.stringify({ memories: [] }); } else { @@ -527,7 +607,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { "content-type": "application/json", ...commonResponseHeaders, }; - options.onResponseStart(200, headers); + options.onResponseStart(statusCode, headers); options.onData(Buffer.from(body)); options.onResponseEnd(); return; @@ -1041,6 +1121,14 @@ function coalesceAdjacentUserMessages(requestBody: string): string { const messages: NonNullable = []; for (const message of request.messages) { + if ( + message.role === "user" && + typeof message.content === "string" && + containsModeChangedNotice(message.content) && + normalizeUserMessage(message.content) === "" + ) { + continue; + } const previous = messages.at(-1); if ( previous?.role === "user" && @@ -1072,6 +1160,70 @@ function openAIErrorBody(code: string | undefined, message: string): unknown { return { error: { message, type, code: type } }; } +function rewriteToolArgumentPaths( + toolName: string, + argumentsJson: string, + rewritePath: (value: string) => string, + rewriteTextWorkDir: (value: string) => string, +): string { + const rewritePatchPaths = (patch: string) => + patch.replace( + /^(\*\*\* (?:Add File|Update File|Delete File|Move to):[ \t]*)([^\r\n]*)/gm, + (_match, header: string, filename: string) => + header + rewritePath(filename), + ); + if ( + toolName === "apply_patch" && + argumentsJson.trimStart().startsWith("*** Begin Patch") + ) { + return rewritePatchPaths(argumentsJson); + } + + let argumentsValue: unknown; + try { + argumentsValue = JSON.parse(argumentsJson, (key, value: unknown) => { + const isPath = /^(?:path|paths|file_path|filePath|cwd|directory)$/.test( + key, + ); + if (typeof value === "string") { + if (toolName === "apply_patch" && (key === "" || key === "input")) { + return rewritePatchPaths(value); + } + if (isPath) { + return rewritePath(value); + } + if (toolName === "${shell}" && key === "command") { + return rewriteTextWorkDir(value); + } + } else if (isPath && Array.isArray(value)) { + return value.map((item: unknown) => + typeof item === "string" ? rewritePath(item) : item, + ); + } + return value; + }); + } catch (error) { + if (error instanceof SyntaxError) { + return argumentsJson; + } + throw error; + } + if ( + toolName === "task" && + typeof argumentsValue === "object" && + argumentsValue !== null && + "prompt" in argumentsValue && + typeof argumentsValue.prompt === "string" + ) { + argumentsValue.prompt = rewriteTextWorkDir(argumentsValue.prompt); + } + const rewritten = JSON.stringify(argumentsValue); + if (rewritten === undefined) { + throw new Error("Unable to encode rewritten tool arguments"); + } + return rewritten; +} + function normalizeFilenames( conversations: NormalizedConversation[], workDir: string, @@ -1095,6 +1247,10 @@ function normalizeFilenames( /(? path.replace(/\\/g, "/"); + const normalizePath = (value: string) => + value.replace(workDirPattern, workDirReplacer).replace(/\\/g, "/"); + const normalizeTextWorkDir = (value: string) => + value.replace(workDirPattern, workDirReplacer); for (const conv of conversations) { for (const msg of conv.messages) { @@ -1104,13 +1260,11 @@ function normalizeFilenames( } for (const tc of msg.tool_calls ?? []) { if (tc.function?.arguments) { - tc.function.arguments = tc.function.arguments.replace( - workDirPattern, - workDirReplacer, - ); - tc.function.arguments = tc.function.arguments.replace( - windowsFnPattern, - windowsFnReplacer, + tc.function.arguments = rewriteToolArgumentPaths( + tc.function.name, + tc.function.arguments, + normalizePath, + normalizeTextWorkDir, ); } } @@ -1135,6 +1289,7 @@ function normalizeToolCalls( const precedingMessages: NormalizedMessage[] = []; let counter = 0; let unnamedBackgroundAgentCounter = 0; + let unnamedAgentCounter = 0; for (const msg of conv.messages) { for (const tc of msg.tool_calls ?? []) { // Normalize ID in tool calls @@ -1163,6 +1318,16 @@ function normalizeToolCalls( tc.function?.name === "read_agent" || tc.function?.name === "write_agent" ) { + for (const runtimeId of getUnnamedRuntimeAgentIds( + tc.function.name, + tc.function.arguments, + (id) => backgroundAgentNamesByRuntimeId.has(id), + )) { + backgroundAgentNamesByRuntimeId.set( + runtimeId, + unnamedAgentName(unnamedAgentCounter++), + ); + } tc.function.arguments = normalizeBackgroundAgentArguments( tc.function.arguments, backgroundAgentNamesByRuntimeId, @@ -1303,6 +1468,77 @@ async function parseHttpExchange( return { request, response, requestHeaders }; } +function addCompactionProviderUsage( + exchanges: ParsedHttpExchange[], + capturedExchanges: readonly CapturedExchange[], +): void { + const chains = new Map< + string, + { + usage: CompactionProviderUsage; + previous: ParsedHttpExchange; + } + >(); + for (const [index, exchange] of exchanges.entries()) { + const status = capturedExchanges[index].response?.statusCode; + const headers = exchange.requestHeaders; + if ( + status === undefined || + status < 200 || + status >= 300 || + headers?.["x-interaction-type"] !== "conversation-compaction" || + !exchange.response?.choices + ) { + continue; + } + const interactionId = headers["x-interaction-id"]; + if (typeof interactionId !== "string" || !interactionId) { + throw new Error( + "Compaction provider response has no interaction identity", + ); + } + const lastMessage = exchange.request.messages.at(-1); + const lastContent = + typeof lastMessage?.content === "string" + ? lastMessage.content + : lastMessage?.role === "user" && Array.isArray(lastMessage.content) + ? lastMessage.content + .map((part) => (part.type === "text" ? part.text : "")) + .join("") + : ""; + // The runtime appends this nudge for both length and reasoning-only continuations. + const continuation = + lastMessage?.role === "user" && + lastContent.endsWith("Please continue from where you left off."); + let chain = continuation ? chains.get(interactionId) : undefined; + if (continuation && !chain) { + throw new Error( + "Compaction continuation has no captured preceding provider response", + ); + } + if (!chain) { + chain = { + usage: { interactionId, summary: "", responseCount: 0, inputTokens: 0 }, + previous: exchange, + }; + chains.set(interactionId, chain); + } else { + delete chain.previous.compactionUsage; + } + const inputTokens = exchange.response.usage?.prompt_tokens; + chain.usage.inputTokens = + typeof inputTokens === "number" && chain.usage.inputTokens !== undefined + ? chain.usage.inputTokens + inputTokens + : undefined; + chain.usage.summary += exchange.response.choices + .map((choice) => choice.message.content ?? "") + .join(""); + chain.usage.responseCount++; + exchange.compactionUsage = { ...chain.usage }; + chain.previous = exchange; + } +} + // Converts a single HTTP exchange (request + response) into a normalized conversation async function transformHttpExchange( requestBody: string, @@ -1312,7 +1548,9 @@ async function transformHttpExchange( requestBody, responseBody, ); - const messages = request.messages.map(transformOpenAIRequestMessage); + const messages = request.messages + .map(transformOpenAIRequestMessage) + .filter((message): message is NormalizedMessage => message !== undefined); if (response?.choices?.length) { messages.push(...transformOpenAIResponseChoice(response.choices)); @@ -1325,29 +1563,41 @@ async function transformHttpExchange( // We use this to look up whether we already have a cached response for it function transformOpenAIRequestMessage( m: ChatCompletionMessageParam, -): NormalizedMessage { +): NormalizedMessage | undefined { let content: string | undefined; + let strippedModeNoticeOnly = false; if (m.role === "system") { // System message changes too often to include in snapshots - just store placeholder content = "${system}"; } else if (m.role === "user" && typeof m.content === "string") { content = normalizeUserMessage(m.content); + strippedModeNoticeOnly = + content === "" && containsModeChangedNotice(m.content); } else if (m.role === "user" && Array.isArray(m.content)) { // Multimodal user messages have array content with text and image_url parts. // Extract and normalize text parts; represent image_url parts as a stable marker. const parts: string[] = []; + let sawModeNotice = false; + let sawVisibleContent = false; for (const part of m.content) { if ( typeof part === "object" && part.type === "text" && typeof part.text === "string" ) { - parts.push(normalizeUserMessage(part.text)); + sawModeNotice ||= containsModeChangedNotice(part.text); + const normalized = normalizeUserMessage(part.text); + if (normalized) { + parts.push(normalized); + sawVisibleContent = true; + } } else if (typeof part === "object" && part.type === "image_url") { parts.push("[image]"); + sawVisibleContent = true; } } content = parts.join("\n") || undefined; + strippedModeNoticeOnly = sawModeNotice && !sawVisibleContent; } else if (m.role === "tool" && typeof m.content === "string") { // If it's a JSON tool call result, normalize the whitespace and property ordering. // For successful tool results wrapped in {resultType, textResultForLlm}, unwrap to @@ -1374,6 +1624,9 @@ function transformOpenAIRequestMessage( content = m.content; } + if (strippedModeNoticeOnly) { + return undefined; + } const msg: NormalizedMessage = { role: m.role }; if ("tool_call_id" in m && m.tool_call_id) { msg.tool_call_id = m.tool_call_id; @@ -1386,7 +1639,7 @@ function transformOpenAIRequestMessage( } function normalizeUserMessage(content: string): string { - return normalizeSkillContextFrontmatter(content) + return stripModeChangedNotice(normalizeSkillContext(content)) .replace( taskCompletionNotificationPattern, taskCompletionNotificationReplacement, @@ -1408,16 +1661,54 @@ const taskCompletionNotificationPattern = const taskCompletionNotificationReplacement = 'Agent "$1" ($2) has completed successfully. Use read_agent with agent_id "$1" to retrieve the full results.'; +const modeChangedNoticePattern = + /(\s*)[\s\S]*?<\/mode_changed_notice>(\s*)/g; + +function containsModeChangedNotice(content: string): boolean { + return /[\s\S]*?<\/mode_changed_notice>/.test(content); +} + +function stripModeChangedNotice(content: string): string { + let removed = false; + const stripped = content.replace( + modeChangedNoticePattern, + ( + match, + leading: string, + trailing: string, + offset: number, + source: string, + ) => { + removed = true; + const before = source.slice(0, offset); + const after = source.slice(offset + match.length); + if (!before.trim() || !after.trim()) { + return ""; + } + return /[\r\n]/.test(leading + trailing) ? "\n\n" : " "; + }, + ); + return removed ? stripped.trim() : content; +} + function normalizeStoredUserMessages(conversations: NormalizedConversation[]) { for (const conversation of conversations) { for (const message of conversation.messages) { if (message.role === "user" && typeof message.content === "string") { - message.content = message.content.replace( - taskCompletionNotificationPattern, - taskCompletionNotificationReplacement, + message.content = stripModeChangedNotice( + normalizeSkillContext(message.content).replace( + taskCompletionNotificationPattern, + taskCompletionNotificationReplacement, + ), ); } } + conversation.messages = conversation.messages.filter( + (message) => + message.role !== "user" || + message.content !== "" || + Object.keys(message).length !== 2, + ); } } @@ -1480,12 +1771,19 @@ function normalizeStoredToolMessages(conversations: NormalizedConversation[]) { } } -function normalizeSkillContextFrontmatter(content: string): string { +function normalizeSkillContext(content: string): string { // Runtime versions may include or omit SKILL.md metadata in the prompt context. - return content.replace( - /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+)---\r?\n(?:(?!<\/skill-context>)[\s\S])*?\r?\n---(?:\r?\n)+/g, - "$1", - ); + return content + .replace( + /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+)---\r?\n(?:(?!<\/skill-context>)[\s\S])*?\r?\n---(?:\r?\n)+/g, + "$1", + ) + .replace( + // Filesystem enumeration order is not part of the OTel skill contract. + /(]*>\s*Base directory for this skill:[^\r\n]*(?:\r?\n)+Related files \(use view tool to read\):\r?\n)((?: - [^\r\n]+\r?\n)+)/g, + (_match, prefix: string, files: string) => + prefix + files.split(/\r?\n/).slice(0, -1).sort().join("\n") + "\n", + ); } function normalizeLargeOutputFilepaths(result: string): string { @@ -1571,6 +1869,7 @@ function normalizeReadAgentResult(result: string): string { /^Agent completed\. (.*), status: idle,/, "Agent completed. $1, status: completed,", ) + .replace(/(, total_turns: \d+), model: [^,\r\n]+/, "$1") .replace(/, total_turns: \d+(?=\r?\n|$)/, ", total_turns: 0, duration: 0s") .replace(/\r?\n\r?\n\[Turn \d+\]\r?\n/, "\n\n"); @@ -1666,6 +1965,51 @@ function replaceBackgroundAgentIds( return normalized; } +const runtimeAgentIdPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Runtime-generated agent IDs a `read_agent` or `write_agent` call names that + * no `task` result introduced, such as the synthetic read the runtime adds when + * an SDK-started agent goes idle. `isKnown` reports IDs that already have a + * stable name. + */ +function getUnnamedRuntimeAgentIds( + toolName: string | undefined, + argumentsJson: string | undefined, + isKnown: (runtimeId: string) => boolean, +): string[] { + if ( + (toolName !== "read_agent" && toolName !== "write_agent") || + !argumentsJson + ) { + return []; + } + try { + const args = JSON.parse(argumentsJson) as { + agent_id?: unknown; + agent_ids?: unknown; + }; + const agentIds = new Set([ + args.agent_id, + ...(Array.isArray(args.agent_ids) ? args.agent_ids : []), + ]); + return [...agentIds].filter( + (agentId): agentId is string => + typeof agentId === "string" && + runtimeAgentIdPattern.test(agentId) && + !isKnown(agentId), + ); + } catch { + return []; + } +} + +/** Stable name for the Nth agent a conversation reads but did not start. */ +function unnamedAgentName(index: number): string { + return index === 0 ? "api-agent" : `api-agent-${index}`; +} + function rewriteBackgroundAgentArguments( argumentsJson: string, replacements: Map, @@ -1728,8 +2072,16 @@ function extractBackgroundAgentIds( }; const backgroundAgentNamesByToolCallId = new Map(); let unnamedBackgroundAgentCounter = 0; + let unnamedAgentCounter = 0; for (const message of request.messages ?? []) { for (const toolCall of message.tool_calls ?? []) { + for (const runtimeId of getUnnamedRuntimeAgentIds( + toolCall.function?.name, + toolCall.function?.arguments, + (id) => [...result.values()].includes(id), + )) { + result.set(unnamedAgentName(unnamedAgentCounter++), runtimeId); + } if ( toolCall.id && toolCall.function?.name === "task" && @@ -1890,8 +2242,33 @@ function findAssistantIndexAfterPrefix( // A continuation can start after an assistant message. Never coalesce // across this candidate request/response boundary. + const savedPrefix = savedMessages.slice(0, nextIndex); + const requestHasRelocatedImage = requestMessages.some( + (message, index) => + message.role === "user" && + /^Image file at path [^\n]+\n\[image\]$/.test(message.content ?? "") && + requestMessages[index - 1]?.role === "tool", + ); const prefix = normalizeMessagesForBackend( - savedMessages.slice(0, nextIndex), + backend === "capi" || requestHasRelocatedImage + ? savedPrefix + : savedPrefix.filter((message, index) => { + const toolResult = savedPrefix[index - 1]; + const toolCall = savedPrefix[index - 2]; + // CAPI sends viewed images as a separate user turn; BYOK includes + // them in (or, for text-only models, omits them from) the tool result. + return !( + message.role === "user" && + /^Image file at path [^\n]+\n\[image\]$/.test( + message.content ?? "", + ) && + toolResult?.role === "tool" && + toolResult.content === "Viewed image file successfully." && + toolCall?.tool_calls?.some( + (call) => call.function?.name === "view", + ) + ); + }), backend, ); if (prefix.length > requestMessages.length) break; @@ -1915,21 +2292,6 @@ function findAssistantIndexAfterPrefix( return undefined; } -function expandWorkDir( - content: string | undefined, - workDir: string, - jsonEscape: boolean, -): string | undefined { - if (!content) { - return content; - } - - const workDirValue = jsonEscape - ? JSON.stringify(workDir).replaceAll('"', "") - : workDir; - return content.replace(/\$\{workdir\}/g, workDirValue); -} - function expandToolName(name: string): string { for (const [fullName, normalized] of Object.entries(normalizedToolNames)) { if (name === normalized) { @@ -1948,6 +2310,8 @@ function createOpenAIResponse( workDir: string, backgroundAgentIds: Map, ): ChatCompletion { + const expandPath = (value: string) => + value.replace(/\$\{workdir\}/g, () => workDir); // Here we recreate the strange CAPI/productcode behavior of using multiple choices to represent // multiple assistant messages in a row. This is the inverse of the logic in transformOpenAIResponseChoice(). // So, find all successive assistant messages starting from responseStartIndex. @@ -1964,7 +2328,12 @@ function createOpenAIResponse( function: { name: expandToolName(tc.function?.name ?? ""), arguments: expandBackgroundAgentArguments( - expandWorkDir(tc.function?.arguments, workDir, true) ?? "{}", + rewriteToolArgumentPaths( + tc.function?.name ?? "", + tc.function?.arguments ?? "{}", + expandPath, + expandPath, + ), backgroundAgentIds, ), }, @@ -1974,8 +2343,9 @@ function createOpenAIResponse( index, message: { role: "assistant", - content: - expandWorkDir(assistantMessage.content, workDir, false) ?? null, + content: assistantMessage.content + ? expandPath(assistantMessage.content) + : null, refusal: assistantMessage.refusal ?? null, tool_calls: toolCalls, }, @@ -2011,7 +2381,8 @@ function convertToStreamingResponseChunks( const toolCalls = completion.choices .flatMap((choice) => choice.message.tool_calls ?? []) .filter( - (tc): tc is ChatCompletionMessageFunctionToolCall => tc.type === "function", + (tc): tc is ChatCompletionMessageFunctionToolCall => + tc.type === "function", ); const makeChunk = ( @@ -2065,19 +2436,22 @@ function convertToStreamingResponseChunks( } chunks[chunks.length - 1].choices[0].finish_reason = toolCalls.length ? "tool_calls" - : completion.choices.at(-1)?.finish_reason ?? "stop"; + : (completion.choices.at(-1)?.finish_reason ?? "stop"); return chunks; } -function createGetModelsResponse(modelIds: string[]) { +function createGetModelsResponse( + modelIds: string[], + modelNames?: Record, +) { // Obviously the following might not match any given model. We could track the original responses from /models, // but that risks invalidating the caches too frequently and making this unmaintainable. If this approximation // turns out to be insufficient, we can tweak the logic here based on known model IDs. return { data: modelIds.map((id) => ({ id, - name: id, + name: modelNames?.[id] ?? id, capabilities: { supports: { vision: true }, limits: { max_context_window_tokens: 128000 }, @@ -2143,10 +2517,36 @@ export type CopilotUserResponse = { >; }; +export type MemoryApiStub = { + enabled?: { enabled: boolean }; + memories?: { + repositoryMemories?: Array<{ + subject: string; + fact: string; + citations: string[]; + }>; + userMemories?: Array<{ + subject: string; + fact: string; + citations: string[]; + }>; + }; + memoriesStatusCode?: number; +}; + export type ParsedHttpExchange = { request: ChatCompletionCreateParamsBase; response: ChatCompletion | undefined; requestHeaders?: Record; + /** Aggregated provider usage on the last successful response of a compaction chain. */ + compactionUsage?: CompactionProviderUsage; +}; + +export type CompactionProviderUsage = { + interactionId: string; + summary: string; + responseCount: number; + inputTokens?: number; }; // We want to be able to reuse the proxy across multiple tests, so it needs to be reconfigurable @@ -2155,6 +2555,7 @@ type ReplayingCapiProxyState = { filePath: string; workDir: string; testInfo?: { file: string; line?: number }; + modelNames?: Record; backend: ReplayBackend; replayOnly: boolean; storedData?: NormalizedData | undefined; diff --git a/test/harness/responsesApiAdapter.ts b/test/harness/responsesApiAdapter.ts index 16568f6e03..68eceb6df6 100644 --- a/test/harness/responsesApiAdapter.ts +++ b/test/harness/responsesApiAdapter.ts @@ -102,10 +102,7 @@ function responseInputItemToCanonicalMessages( { role: "tool", tool_call_id: typeof item.call_id === "string" ? item.call_id : "", - content: - typeof item.output === "string" - ? item.output - : JSON.stringify(item.output ?? ""), + content: normalizeFunctionCallOutput(item.output), }, ]; } @@ -180,6 +177,35 @@ function responseInputItemToCanonicalMessages( ]; } +function normalizeFunctionCallOutput(output: unknown): string { + const text = + typeof output === "string" ? output : JSON.stringify(output ?? ""); + if (!text.startsWith("[{") || !text.includes('"input_image"')) return text; + + let parts: unknown; + try { + parts = JSON.parse(text); + } catch { + return text; + } + if ( + !Array.isArray(parts) || + !parts.some((part) => isObject(part) && part.type === "input_image") || + !parts.every( + (part) => + isObject(part) && + ((part.type === "input_text" && typeof part.text === "string") || + (part.type === "input_image" && typeof part.image_url === "string")), + ) + ) { + return text; + } + return parts + .filter((part) => part.type === "input_text") + .map((part) => part.text) + .join("\n"); +} + function coalesceAssistantMessages( messages: CanonicalMessage[], ): CanonicalMessage[] { diff --git a/test/harness/stdio-shutdown-runtime.cjs b/test/harness/stdio-shutdown-runtime.cjs index 99d1c8cec2..fe7dc1cc09 100644 --- a/test/harness/stdio-shutdown-runtime.cjs +++ b/test/harness/stdio-shutdown-runtime.cjs @@ -4,7 +4,7 @@ // Shared SDK shutdown fixture: node